From f03ecaf4271d5dbaa605b31610b97171b9213d1a Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Thu, 4 Jan 2018 20:39:07 +0000 Subject: [PATCH] Import snapd_2.30.orig.tar.gz [dgit import orig snapd_2.30.orig.tar.gz] --- .gitignore | 67 + .travis.yml | 22 + CONTRIBUTING.md | 27 + COPYING | 674 ++ HACKING.md | 200 + PULL_REQUEST_TEMPLATE.md | 2 + README.md | 48 + arch/arch.go | 154 + arch/arch_test.go | 61 + asserts/account.go | 106 + asserts/account_key.go | 288 + asserts/account_key_test.go | 809 ++ asserts/account_test.go | 169 + asserts/asserts.go | 1017 +++ asserts/asserts_test.go | 899 ++ asserts/assertstest/assertstest.go | 446 + asserts/assertstest/assertstest_test.go | 163 + asserts/crypto.go | 398 + asserts/database.go | 623 ++ asserts/database_test.go | 1190 +++ asserts/device_asserts.go | 444 + asserts/device_asserts_test.go | 576 ++ asserts/digest.go | 43 + asserts/digest_test.go | 65 + asserts/export_test.go | 193 + asserts/fetcher.go | 121 + asserts/fetcher_test.go | 167 + asserts/findwildcard.go | 111 + asserts/findwildcard_test.go | 139 + asserts/fsbackstore.go | 221 + asserts/fsbackstore_test.go | 258 + asserts/fsentryutils.go | 70 + asserts/fskeypairmgr.go | 92 + asserts/fskeypairmgr_test.go | 65 + asserts/gpgkeypairmgr.go | 359 + asserts/gpgkeypairmgr_test.go | 331 + asserts/header_checks.go | 272 + asserts/headers.go | 318 + asserts/headers_test.go | 396 + asserts/ifacedecls.go | 974 ++ asserts/ifacedecls_test.go | 1362 +++ asserts/membackstore.go | 191 + asserts/membackstore_test.go | 351 + asserts/memkeypairmgr.go | 59 + asserts/memkeypairmgr_test.go | 73 + asserts/privkeys_for_test.go | 54 + asserts/repair.go | 159 + asserts/repair_test.go | 183 + asserts/signtool/sign.go | 88 + asserts/signtool/sign_test.go | 179 + asserts/snap_asserts.go | 931 ++ asserts/snap_asserts_test.go | 1776 ++++ asserts/snapasserts/snapasserts.go | 145 + asserts/snapasserts/snapasserts_test.go | 315 + asserts/store_asserts.go | 147 + asserts/store_asserts_test.go | 219 + asserts/sysdb/generic.go | 196 + asserts/sysdb/staging.go | 183 + asserts/sysdb/sysdb.go | 49 + asserts/sysdb/sysdb_test.go | 212 + asserts/sysdb/testkeys.go | 30 + asserts/sysdb/trusted.go | 156 + asserts/systestkeys/trusted.go | 263 + asserts/user.go | 263 + asserts/user_test.go | 200 + boot/boottest/mockbootloader.go | 72 + boot/kernel_os.go | 204 + boot/kernel_os_test.go | 251 + client/aliases.go | 102 + client/aliases_test.go | 195 + client/apps.go | 242 + client/apps_test.go | 372 + client/asserts.go | 104 + client/asserts_test.go | 159 + client/buy.go | 53 + client/change.go | 164 + client/change_test.go | 215 + client/client.go | 576 ++ client/client_test.go | 513 ++ client/conf.go | 50 + client/conf_test.go | 104 + client/export_test.go | 45 + client/icons.go | 66 + client/icons_test.go | 65 + client/interfaces.go | 155 + client/interfaces_test.go | 299 + client/login.go | 161 + client/login_test.go | 132 + client/packages.go | 233 + client/packages_test.go | 283 + client/snap_op.go | 260 + client/snap_op_test.go | 309 + client/snapctl.go | 57 + client/snapctl_test.go | 68 + cmd/.indent.pro | 34 + cmd/Makefile.am | 424 + cmd/autogen.sh | 59 + cmd/cmd.go | 205 + cmd/cmd_test.go | 307 + cmd/configure.ac | 213 + cmd/decode-mount-opts/decode-mount-opts.c | 38 + cmd/export_test.go | 60 + .../cgroup-freezer-support.c | 67 + .../cgroup-freezer-support.h | 26 + cmd/libsnap-confine-private/classic-test.c | 71 + cmd/libsnap-confine-private/classic.c | 25 + cmd/libsnap-confine-private/classic.h | 27 + .../cleanup-funcs-test.c | 44 + cmd/libsnap-confine-private/cleanup-funcs.c | 56 + cmd/libsnap-confine-private/cleanup-funcs.h | 74 + cmd/libsnap-confine-private/error-test.c | 256 + cmd/libsnap-confine-private/error.c | 147 + cmd/libsnap-confine-private/error.h | 163 + .../fault-injection-test.c | 63 + cmd/libsnap-confine-private/fault-injection.c | 78 + cmd/libsnap-confine-private/fault-injection.h | 67 + cmd/libsnap-confine-private/locking-test.c | 109 + cmd/libsnap-confine-private/locking.c | 145 + cmd/libsnap-confine-private/locking.h | 84 + cmd/libsnap-confine-private/mount-opt-test.c | 261 + cmd/libsnap-confine-private/mount-opt.c | 321 + cmd/libsnap-confine-private/mount-opt.h | 75 + cmd/libsnap-confine-private/mountinfo-test.c | 200 + cmd/libsnap-confine-private/mountinfo.c | 274 + cmd/libsnap-confine-private/mountinfo.h | 135 + cmd/libsnap-confine-private/privs-test.c | 67 + cmd/libsnap-confine-private/privs.c | 78 + cmd/libsnap-confine-private/privs.h | 38 + .../secure-getenv-test.c | 23 + cmd/libsnap-confine-private/secure-getenv.c | 31 + cmd/libsnap-confine-private/secure-getenv.h | 36 + cmd/libsnap-confine-private/snap-test.c | 207 + cmd/libsnap-confine-private/snap.c | 162 + cmd/libsnap-confine-private/snap.h | 61 + .../string-utils-test.c | 839 ++ cmd/libsnap-confine-private/string-utils.c | 244 + cmd/libsnap-confine-private/string-utils.h | 106 + cmd/libsnap-confine-private/test-utils-test.c | 45 + cmd/libsnap-confine-private/test-utils.c | 73 + cmd/libsnap-confine-private/test-utils.h | 26 + cmd/libsnap-confine-private/unit-tests-main.c | 22 + cmd/libsnap-confine-private/unit-tests.c | 29 + cmd/libsnap-confine-private/unit-tests.h | 29 + cmd/libsnap-confine-private/utils-test.c | 187 + cmd/libsnap-confine-private/utils.c | 219 + cmd/libsnap-confine-private/utils.h | 63 + cmd/snap-confine/PORTING | 15 + cmd/snap-confine/README.mount_namespace | 138 + cmd/snap-confine/README.nvidia | 26 + cmd/snap-confine/README.syscalls | 436 + cmd/snap-confine/apparmor-support.c | 141 + cmd/snap-confine/apparmor-support.h | 93 + cmd/snap-confine/cookie-support-test.c | 102 + cmd/snap-confine/cookie-support.c | 75 + cmd/snap-confine/cookie-support.h | 34 + ...printk-based-debugging-to-pivot_root.patch | 132 + cmd/snap-confine/mount-support-nvidia.c | 366 + cmd/snap-confine/mount-support-nvidia.h | 48 + cmd/snap-confine/mount-support-test.c | 100 + cmd/snap-confine/mount-support.c | 715 ++ cmd/snap-confine/mount-support.h | 44 + cmd/snap-confine/ns-support-test.c | 212 + cmd/snap-confine/ns-support.c | 578 ++ cmd/snap-confine/ns-support.h | 147 + cmd/snap-confine/quirks.c | 230 + cmd/snap-confine/quirks.h | 30 + cmd/snap-confine/seccomp-support.c | 221 + cmd/snap-confine/seccomp-support.h | 28 + cmd/snap-confine/snap-confine-args-test.c | 482 + cmd/snap-confine/snap-confine-args.c | 263 + cmd/snap-confine/snap-confine-args.h | 124 + cmd/snap-confine/snap-confine.apparmor.in | 559 ++ cmd/snap-confine/snap-confine.c | 340 + cmd/snap-confine/snap-confine.rst | 200 + cmd/snap-confine/snappy-app-dev | 39 + .../spread-tests/data/apt-keys/README.md | 4 + .../spread-tests/data/apt-keys/sbuild-key.pub | Bin 0 -> 427 bytes .../spread-tests/data/apt-keys/sbuild-key.sec | Bin 0 -> 759 bytes cmd/snap-confine/spread-tests/distros/debian. | 2 + .../spread-tests/distros/debian.common | 12 + .../spread-tests/distros/ubuntu.14.04 | 2 + .../spread-tests/distros/ubuntu.16.04 | 2 + .../spread-tests/distros/ubuntu.16.10 | 2 + .../spread-tests/distros/ubuntu.common | 7 + .../spread-tests/main/cgroup-used/task.yaml | 37 + .../main/core-is-preferred/task.yaml | 10 + .../spread-tests/main/debug-flags/task.yaml | 12 + .../main/hostfs-created-on-demand/task.yaml | 24 + .../main/media-visible-in-devmode/task.yaml | 15 + .../expected.classic.core.json | 1028 +++ .../expected.classic.core.linode.amd64.json | 798 ++ .../expected.classic.core.linode.i386.json | 808 ++ .../expected.classic.core.qemu.amd64.json | 808 ++ .../expected.classic.core.qemu.i386.json | 808 ++ .../expected.classic.ubuntu-core.json | 1028 +++ ...cted.classic.ubuntu-core.linode.amd64.json | 788 ++ ...ected.classic.ubuntu-core.linode.i386.json | 798 ++ ...pected.classic.ubuntu-core.qemu.amd64.json | 798 ++ ...xpected.classic.ubuntu-core.qemu.i386.json | 798 ++ .../main/mount-ns-layout/expected.core.json | 2050 +++++ .../mount-ns-layout/expected.core.linode.json | 1800 ++++ .../main/mount-ns-layout/process.py | 117 + .../main/mount-ns-layout/snap-arch.py | 22 + .../main/mount-ns-layout/task.yaml | 46 + .../main/mount-ns-sharing/task.yaml | 20 + .../task.yaml | 26 + .../mount-profiles-bin-snap-source/task.yaml | 26 + .../main/mount-profiles-missing-dst/task.yaml | 19 + .../main/mount-profiles-missing-src/task.yaml | 19 + .../main/mount-profiles-mount-tmpfs/task.yaml | 20 + .../main/mount-profiles-ro-mount/task.yaml | 18 + .../main/mount-profiles-rw-mount/task.yaml | 24 + .../spread-tests/main/mount-usr-src/task.yaml | 17 + .../main/test-seccomp-compat/task.yaml | 16 + .../main/test-snap-runs/task.yaml | 15 + .../ubuntu-core-launcher-exists/task.yaml | 6 + .../main/user-data-dir-created/task.yaml | 23 + .../user-xdg-runtime-dir-created/task.yaml | 21 + .../regression/lp-1599608/task.yaml | 69 + cmd/snap-confine/spread-tests/release.sh | 41 + .../spread-tests/spread-prepare.sh | 179 + cmd/snap-confine/udev-support.c | 292 + cmd/snap-confine/udev-support.h | 40 + cmd/snap-confine/user-support.c | 65 + cmd/snap-confine/user-support.h | 25 + cmd/snap-discard-ns/snap-discard-ns.c | 55 + cmd/snap-discard-ns/snap-discard-ns.rst | 53 + cmd/snap-exec/export_test.go | 51 + cmd/snap-exec/main.go | 232 + cmd/snap-exec/main_test.go | 383 + cmd/snap-repair/cmd_done_retry_skip.go | 82 + cmd/snap-repair/cmd_done_retry_skip_test.go | 81 + cmd/snap-repair/cmd_list.go | 74 + cmd/snap-repair/cmd_list_test.go | 47 + cmd/snap-repair/cmd_run.go | 102 + cmd/snap-repair/cmd_run_test.go | 72 + cmd/snap-repair/cmd_show.go | 91 + cmd/snap-repair/cmd_show_test.go | 142 + cmd/snap-repair/export_test.go | 142 + cmd/snap-repair/main.go | 89 + cmd/snap-repair/main_test.go | 87 + cmd/snap-repair/runner.go | 991 +++ cmd/snap-repair/runner_test.go | 1776 ++++ cmd/snap-repair/staging.go | 81 + cmd/snap-repair/trace.go | 176 + cmd/snap-repair/trace_test.go | 66 + cmd/snap-repair/trusted.go | 89 + cmd/snap-seccomp/export_test.go | 41 + cmd/snap-seccomp/main.go | 723 ++ cmd/snap-seccomp/main_ppc64le.go | 31 + cmd/snap-seccomp/main_test.go | 794 ++ cmd/snap-update-ns/bootstrap.c | 271 + cmd/snap-update-ns/bootstrap.go | 117 + cmd/snap-update-ns/bootstrap.h | 33 + cmd/snap-update-ns/bootstrap_ppc64le.go | 31 + cmd/snap-update-ns/bootstrap_test.go | 87 + cmd/snap-update-ns/change.go | 216 + cmd/snap-update-ns/change_test.go | 488 + cmd/snap-update-ns/entry.go | 129 + cmd/snap-update-ns/entry_test.go | 180 + cmd/snap-update-ns/export_test.go | 341 + cmd/snap-update-ns/freezer.go | 74 + cmd/snap-update-ns/freezer_test.go | 95 + cmd/snap-update-ns/main.go | 208 + cmd/snap-update-ns/main_test.go | 220 + cmd/snap-update-ns/sorting.go | 44 + cmd/snap-update-ns/sorting_test.go | 50 + cmd/snap-update-ns/utils.go | 387 + cmd/snap-update-ns/utils_test.go | 538 ++ cmd/snap/cmd_abort.go | 60 + cmd/snap/cmd_ack.go | 77 + cmd/snap/cmd_alias.go | 115 + cmd/snap/cmd_alias_test.go | 78 + cmd/snap/cmd_aliases.go | 133 + cmd/snap/cmd_aliases_test.go | 168 + cmd/snap/cmd_auto_import.go | 300 + cmd/snap/cmd_auto_import_test.go | 302 + cmd/snap/cmd_booted.go | 50 + cmd/snap/cmd_buy.go | 138 + cmd/snap/cmd_buy_test.go | 450 + cmd/snap/cmd_can_manage_refreshes.go | 51 + cmd/snap/cmd_changes.go | 168 + cmd/snap/cmd_changes_test.go | 180 + cmd/snap/cmd_confinement.go | 54 + cmd/snap/cmd_confinement_test.go | 39 + cmd/snap/cmd_connect.go | 87 + cmd/snap/cmd_connect_test.go | 329 + cmd/snap/cmd_create_key.go | 88 + cmd/snap/cmd_create_key_test.go | 34 + cmd/snap/cmd_create_user.go | 115 + cmd/snap/cmd_create_user_test.go | 150 + cmd/snap/cmd_debug.go | 34 + cmd/snap/cmd_delete_key.go | 57 + cmd/snap/cmd_delete_key_test.go | 64 + cmd/snap/cmd_disconnect.go | 89 + cmd/snap/cmd_disconnect_test.go | 228 + cmd/snap/cmd_download.go | 134 + cmd/snap/cmd_ensure_state_soon.go | 44 + cmd/snap/cmd_ensure_state_soon_test.go | 55 + cmd/snap/cmd_export_key.go | 97 + cmd/snap/cmd_export_key_test.go | 84 + cmd/snap/cmd_find.go | 151 + cmd/snap/cmd_find_test.go | 364 + cmd/snap/cmd_first_boot.go | 49 + cmd/snap/cmd_get.go | 262 + cmd/snap/cmd_get_base_declaration.go | 51 + cmd/snap/cmd_get_base_declaration_test.go | 55 + cmd/snap/cmd_get_test.go | 226 + cmd/snap/cmd_help.go | 62 + cmd/snap/cmd_help_test.go | 81 + cmd/snap/cmd_info.go | 367 + cmd/snap/cmd_info_test.go | 187 + cmd/snap/cmd_interface.go | 186 + cmd/snap/cmd_interface_test.go | 293 + cmd/snap/cmd_interfaces.go | 141 + cmd/snap/cmd_interfaces_test.go | 574 ++ cmd/snap/cmd_keys.go | 90 + cmd/snap/cmd_keys_test.go | 127 + cmd/snap/cmd_known.go | 129 + cmd/snap/cmd_known_test.go | 109 + cmd/snap/cmd_list.go | 105 + cmd/snap/cmd_list_test.go | 211 + cmd/snap/cmd_login.go | 129 + cmd/snap/cmd_login_test.go | 87 + cmd/snap/cmd_logout.go | 49 + cmd/snap/cmd_managed.go | 55 + cmd/snap/cmd_managed_test.go | 46 + cmd/snap/cmd_pack.go | 66 + cmd/snap/cmd_prefer.go | 69 + cmd/snap/cmd_prefer_test.go | 75 + cmd/snap/cmd_prepare_image.go | 77 + cmd/snap/cmd_repair_repairs.go | 93 + cmd/snap/cmd_repair_repairs_test.go | 67 + cmd/snap/cmd_run.go | 483 + cmd/snap/cmd_run_test.go | 613 ++ cmd/snap/cmd_services.go | 215 + cmd/snap/cmd_services_test.go | 173 + cmd/snap/cmd_set.go | 96 + cmd/snap/cmd_set_test.go | 118 + cmd/snap/cmd_shell.go | 100 + cmd/snap/cmd_sign.go | 79 + cmd/snap/cmd_sign_build.go | 117 + cmd/snap/cmd_sign_build_test.go | 134 + cmd/snap/cmd_sign_test.go | 65 + cmd/snap/cmd_snap_op.go | 1018 +++ cmd/snap/cmd_snap_op_test.go | 1044 +++ cmd/snap/cmd_unalias.go | 68 + cmd/snap/cmd_unalias_test.go | 75 + cmd/snap/cmd_userd.go | 74 + cmd/snap/cmd_userd_test.go | 87 + cmd/snap/cmd_version.go | 78 + cmd/snap/cmd_version_test.go | 59 + cmd/snap/cmd_watch.go | 54 + cmd/snap/cmd_watch_test.go | 73 + cmd/snap/cmd_whoami.go | 56 + cmd/snap/complete.go | 457 + cmd/snap/error.go | 176 + cmd/snap/export_test.go | 140 + cmd/snap/gnupg2_test.go | 27 + cmd/snap/interfaces_common.go | 85 + cmd/snap/interfaces_common_test.go | 106 + cmd/snap/last.go | 87 + cmd/snap/main.go | 352 + cmd/snap/main_test.go | 283 + cmd/snap/notes.go | 169 + cmd/snap/notes_test.go | 107 + cmd/snap/test-data/pubring.gpg | Bin 0 -> 2192 bytes cmd/snap/test-data/secring.gpg | Bin 0 -> 4774 bytes cmd/snap/test-data/trustdb.gpg | Bin 0 -> 1360 bytes cmd/snapctl/main.go | 78 + cmd/snapctl/main_test.go | 117 + cmd/snapd/main.go | 85 + .../system-shutdown-utils-test.c | 21 + cmd/system-shutdown/system-shutdown-utils.c | 160 + cmd/system-shutdown/system-shutdown-utils.h | 37 + cmd/system-shutdown/system-shutdown.c | 123 + cmd/version.go | 31 + corecfg/corecfg.go | 87 + corecfg/corecfg_test.go | 86 + corecfg/export_test.go | 27 + corecfg/picfg.go | 98 + corecfg/picfg_test.go | 154 + corecfg/powerbtn.go | 81 + corecfg/powerbtn_test.go | 79 + corecfg/proxy.go | 96 + corecfg/proxy_test.go | 144 + corecfg/refresh.go | 51 + corecfg/refresh_test.go | 50 + corecfg/services.go | 81 + corecfg/services_test.go | 141 + corecfg/utils.go | 97 + corecfg/utils_test.go | 65 + daemon/api.go | 2702 ++++++ daemon/api_mock_test.go | 124 + daemon/api_test.go | 6292 +++++++++++++ daemon/daemon.go | 484 + daemon/daemon_test.go | 527 ++ daemon/response.go | 346 + daemon/response_test.go | 109 + daemon/snap.go | 403 + daemon/ucrednet.go | 113 + daemon/ucrednet_test.go | 179 + data/Makefile | 4 + data/completion/complete.sh | 129 + data/completion/etelpmoc.sh | 212 + data/completion/snap | 75 + data/dbus/Makefile | 33 + data/dbus/io.snapcraft.Launcher.service.in | 3 + data/env/Makefile | 37 + data/env/snapd.sh.in | 14 + data/failure.txt | 8 + data/polkit/io.snapcraft.snapd.policy | 29 + data/selinux/COPYING | 339 + data/selinux/INSTALL.md | 32 + data/selinux/Makefile | 35 + data/selinux/README.md | 25 + data/selinux/snappy.fc | 41 + data/selinux/snappy.if | 273 + data/selinux/snappy.te | 217 + data/success.txt | 20 + data/systemd/Makefile | 42 + data/systemd/snapd.autoimport.service.in | 12 + data/systemd/snapd.core-fixup.service.in | 16 + data/systemd/snapd.core-fixup.sh | 43 + data/systemd/snapd.refresh.service.in | 11 + data/systemd/snapd.refresh.timer | 14 + data/systemd/snapd.service.in | 16 + data/systemd/snapd.snap-repair.service.in | 10 + data/systemd/snapd.snap-repair.timer | 14 + data/systemd/snapd.socket | 13 + data/systemd/snapd.system-shutdown.service.in | 15 + data/udev/rules.d/66-snapd-autoimport.rules | 3 + debian | 1 + dirs/dirs.go | 248 + dirs/dirs_test.go | 105 + docs/MOVED.md | 1 + errtracker/errtracker.go | 235 + errtracker/errtracker_test.go | 289 + errtracker/export_test.go | 72 + gen-coverage.sh | 9 + generate-packaging-dir | 17 + get-deps.sh | 23 + httputil/export_test.go | 34 + httputil/logger.go | 135 + httputil/logger_test.go | 182 + httputil/redirect17.go | 40 + httputil/redirect18.go | 28 + httputil/retry.go | 135 + httputil/retry_test.go | 429 + httputil/transport16.go | 41 + httputil/transport17.go | 43 + httputil/useragent.go | 92 + httputil/useragent_test.go | 79 + httputil/withtestkeys.go | 26 + i18n/i18n.go | 106 + i18n/i18n_test.go | 162 + i18n/xgettext-go/main.go | 306 + i18n/xgettext-go/main_test.go | 516 ++ image/export_test.go | 40 + image/helpers.go | 233 + image/image.go | 510 ++ image/image_test.go | 828 ++ interfaces/apparmor/apparmor.go | 110 + interfaces/apparmor/apparmor_test.go | 185 + interfaces/apparmor/backend.go | 444 + interfaces/apparmor/backend_test.go | 916 ++ interfaces/apparmor/export_test.go | 104 + interfaces/apparmor/spec.go | 128 + interfaces/apparmor/spec_test.go | 101 + interfaces/apparmor/template.go | 501 ++ interfaces/apparmor/template_vars.go | 40 + interfaces/backend.go | 94 + interfaces/backends/backends.go | 72 + interfaces/backends/backends_test.go | 55 + interfaces/backends/export_test.go | 24 + interfaces/builtin/account_control.go | 122 + interfaces/builtin/account_control_test.go | 113 + interfaces/builtin/all.go | 136 + interfaces/builtin/all_test.go | 481 + interfaces/builtin/alsa.go | 70 + interfaces/builtin/alsa_test.go | 109 + interfaces/builtin/autopilot.go | 76 + interfaces/builtin/autopilot_test.go | 101 + interfaces/builtin/avahi_control.go | 175 + interfaces/builtin/avahi_control_test.go | 195 + interfaces/builtin/avahi_observe.go | 470 + interfaces/builtin/avahi_observe_test.go | 195 + interfaces/builtin/bluetooth_control.go | 73 + interfaces/builtin/bluetooth_control_test.go | 115 + interfaces/builtin/bluez.go | 271 + interfaces/builtin/bluez_test.go | 263 + interfaces/builtin/bool_file.go | 145 + interfaces/builtin/bool_file_test.go | 204 + interfaces/builtin/broadcom_asic_control.go | 79 + .../builtin/broadcom_asic_control_test.go | 122 + interfaces/builtin/browser_support.go | 341 + interfaces/builtin/browser_support_test.go | 201 + interfaces/builtin/camera.go | 58 + interfaces/builtin/camera_test.go | 109 + interfaces/builtin/classic_support.go | 130 + interfaces/builtin/classic_support_test.go | 96 + interfaces/builtin/common.go | 154 + interfaces/builtin/common_test.go | 82 + interfaces/builtin/content.go | 234 + interfaces/builtin/content_test.go | 378 + interfaces/builtin/core_support.go | 117 + interfaces/builtin/core_support_test.go | 99 + interfaces/builtin/cups_control.go | 49 + interfaces/builtin/dbus.go | 424 + interfaces/builtin/dbus_test.go | 673 ++ interfaces/builtin/dcdbas_control.go | 64 + interfaces/builtin/dcdbas_control_test.go | 91 + interfaces/builtin/desktop.go | 208 + interfaces/builtin/desktop_legacy.go | 236 + interfaces/builtin/desktop_legacy_test.go | 104 + interfaces/builtin/desktop_test.go | 153 + interfaces/builtin/docker.go | 83 + interfaces/builtin/docker_support.go | 590 ++ interfaces/builtin/docker_support_test.go | 190 + interfaces/builtin/docker_test.go | 94 + interfaces/builtin/export_test.go | 78 + interfaces/builtin/firewall_control.go | 167 + interfaces/builtin/firewall_control_test.go | 119 + interfaces/builtin/framebuffer.go | 53 + interfaces/builtin/framebuffer_test.go | 111 + interfaces/builtin/fuse_support.go | 100 + interfaces/builtin/fuse_support_test.go | 118 + interfaces/builtin/fwupd.go | 274 + interfaces/builtin/fwupd_test.go | 172 + interfaces/builtin/gpg_keys.go | 59 + interfaces/builtin/gpg_keys_test.go | 91 + interfaces/builtin/gpg_public_keys.go | 62 + interfaces/builtin/gpg_public_keys_test.go | 91 + interfaces/builtin/gpio.go | 122 + interfaces/builtin/gpio_test.go | 143 + interfaces/builtin/greengrass_support.go | 201 + interfaces/builtin/greengrass_support_test.go | 98 + interfaces/builtin/gsettings.go | 56 + interfaces/builtin/gsettings_test.go | 109 + interfaces/builtin/hardware_observe.go | 109 + interfaces/builtin/hardware_observe_test.go | 103 + interfaces/builtin/hardware_random_control.go | 61 + .../builtin/hardware_random_control_test.go | 109 + interfaces/builtin/hardware_random_observe.go | 56 + .../builtin/hardware_random_observe_test.go | 109 + interfaces/builtin/hidraw.go | 205 + interfaces/builtin/hidraw_test.go | 320 + interfaces/builtin/home.go | 69 + interfaces/builtin/home_test.go | 92 + interfaces/builtin/i2c.go | 123 + interfaces/builtin/i2c_test.go | 183 + interfaces/builtin/iio.go | 135 + interfaces/builtin/iio_test.go | 189 + interfaces/builtin/io_ports_control.go | 65 + interfaces/builtin/io_ports_control_test.go | 117 + interfaces/builtin/joystick.go | 60 + interfaces/builtin/joystick_test.go | 109 + interfaces/builtin/kernel_module_control.go | 79 + .../builtin/kernel_module_control_test.go | 117 + interfaces/builtin/kubernetes_support.go | 99 + interfaces/builtin/kubernetes_support_test.go | 101 + interfaces/builtin/kvm.go | 52 + interfaces/builtin/kvm_test.go | 114 + interfaces/builtin/libvirt.go | 53 + interfaces/builtin/libvirt_test.go | 69 + interfaces/builtin/locale_control.go | 49 + interfaces/builtin/locale_control_test.go | 94 + interfaces/builtin/location_control.go | 256 + interfaces/builtin/location_control_test.go | 214 + interfaces/builtin/location_observe.go | 310 + interfaces/builtin/location_observe_test.go | 208 + interfaces/builtin/log_observe.go | 74 + interfaces/builtin/log_observe_test.go | 92 + interfaces/builtin/lxd.go | 54 + interfaces/builtin/lxd_support.go | 67 + interfaces/builtin/lxd_support_test.go | 109 + interfaces/builtin/lxd_test.go | 107 + interfaces/builtin/maliit.go | 173 + interfaces/builtin/maliit_test.go | 264 + interfaces/builtin/media_hub.go | 204 + interfaces/builtin/media_hub_test.go | 196 + interfaces/builtin/mir.go | 146 + interfaces/builtin/mir_test.go | 158 + interfaces/builtin/modem_manager.go | 1258 +++ interfaces/builtin/modem_manager_test.go | 250 + interfaces/builtin/mount_observe.go | 77 + interfaces/builtin/mount_observe_test.go | 91 + interfaces/builtin/mpris.go | 236 + interfaces/builtin/mpris_test.go | 346 + interfaces/builtin/netlink_audit.go | 48 + interfaces/builtin/netlink_audit_test.go | 92 + interfaces/builtin/netlink_connector.go | 51 + interfaces/builtin/netlink_connector_test.go | 92 + interfaces/builtin/network.go | 89 + interfaces/builtin/network_bind.go | 109 + interfaces/builtin/network_bind_test.go | 100 + interfaces/builtin/network_control.go | 276 + interfaces/builtin/network_control_test.go | 116 + interfaces/builtin/network_manager.go | 487 + interfaces/builtin/network_manager_test.go | 206 + interfaces/builtin/network_observe.go | 162 + interfaces/builtin/network_observe_test.go | 101 + interfaces/builtin/network_setup_control.go | 49 + .../builtin/network_setup_control_test.go | 91 + interfaces/builtin/network_setup_observe.go | 49 + .../builtin/network_setup_observe_test.go | 92 + interfaces/builtin/network_status.go | 154 + interfaces/builtin/network_status_test.go | 103 + interfaces/builtin/network_test.go | 101 + interfaces/builtin/ofono.go | 363 + interfaces/builtin/ofono_test.go | 216 + interfaces/builtin/online_accounts_service.go | 144 + .../builtin/online_accounts_service_test.go | 106 + interfaces/builtin/opengl.go | 109 + interfaces/builtin/opengl_test.go | 109 + interfaces/builtin/openvswitch.go | 45 + interfaces/builtin/openvswitch_support.go | 49 + .../builtin/openvswitch_support_test.go | 113 + interfaces/builtin/openvswitch_test.go | 91 + interfaces/builtin/optical_drive.go | 54 + interfaces/builtin/optical_drive_test.go | 109 + .../builtin/password_manager_service.go | 93 + .../builtin/password_manager_service_test.go | 93 + interfaces/builtin/physical_memory_control.go | 57 + .../builtin/physical_memory_control_test.go | 109 + interfaces/builtin/physical_memory_observe.go | 53 + .../builtin/physical_memory_observe_test.go | 110 + interfaces/builtin/ppp.go | 75 + interfaces/builtin/ppp_test.go | 119 + interfaces/builtin/process_control.go | 70 + interfaces/builtin/process_control_test.go | 100 + interfaces/builtin/pulseaudio.go | 181 + interfaces/builtin/pulseaudio_test.go | 130 + interfaces/builtin/raw_usb.go | 61 + interfaces/builtin/raw_usb_test.go | 109 + interfaces/builtin/removable_media.go | 57 + interfaces/builtin/removable_media_test.go | 91 + interfaces/builtin/screen_inhibit_control.go | 93 + .../builtin/screen_inhibit_control_test.go | 92 + interfaces/builtin/serial_port.go | 226 + interfaces/builtin/serial_port_test.go | 477 + interfaces/builtin/shutdown.go | 77 + interfaces/builtin/shutdown_test.go | 89 + interfaces/builtin/snapd_control.go | 75 + interfaces/builtin/snapd_control_test.go | 115 + interfaces/builtin/spi.go | 105 + interfaces/builtin/spi_test.go | 190 + interfaces/builtin/ssh_keys.go | 51 + interfaces/builtin/ssh_keys_test.go | 91 + interfaces/builtin/ssh_public_keys.go | 51 + interfaces/builtin/ssh_public_keys_test.go | 91 + .../builtin/storage_framework_service.go | 162 + .../builtin/storage_framework_service_test.go | 100 + interfaces/builtin/system_observe.go | 123 + interfaces/builtin/system_observe_test.go | 101 + interfaces/builtin/system_trace.go | 75 + interfaces/builtin/system_trace_test.go | 91 + interfaces/builtin/thumbnailer_service.go | 146 + .../builtin/thumbnailer_service_test.go | 116 + interfaces/builtin/time_control.go | 115 + interfaces/builtin/time_control_test.go | 117 + interfaces/builtin/timeserver_control.go | 100 + interfaces/builtin/timeserver_control_test.go | 92 + interfaces/builtin/timezone_control.go | 102 + interfaces/builtin/timezone_control_test.go | 92 + interfaces/builtin/tpm.go | 51 + interfaces/builtin/tpm_test.go | 109 + interfaces/builtin/ubuntu_download_manager.go | 246 + .../builtin/ubuntu_download_manager_test.go | 91 + interfaces/builtin/udisks2.go | 421 + interfaces/builtin/udisks2_test.go | 198 + interfaces/builtin/uhid.go | 52 + interfaces/builtin/uhid_test.go | 100 + interfaces/builtin/unity7.go | 631 ++ interfaces/builtin/unity7_test.go | 102 + interfaces/builtin/unity8.go | 133 + interfaces/builtin/unity8_calendar.go | 157 + interfaces/builtin/unity8_calendar_test.go | 217 + interfaces/builtin/unity8_contacts.go | 194 + interfaces/builtin/unity8_contacts_test.go | 220 + interfaces/builtin/unity8_pim_common.go | 171 + interfaces/builtin/unity8_test.go | 125 + interfaces/builtin/upower_observe.go | 284 + interfaces/builtin/upower_observe_test.go | 246 + interfaces/builtin/utils.go | 98 + interfaces/builtin/utils_test.go | 72 + interfaces/builtin/wayland.go | 50 + interfaces/builtin/wayland_test.go | 102 + interfaces/builtin/x11.go | 75 + interfaces/builtin/x11_test.go | 103 + interfaces/connection.go | 249 + interfaces/connection_test.go | 189 + interfaces/core.go | 267 + interfaces/core_test.go | 222 + interfaces/dbus/backend.go | 161 + interfaces/dbus/backend_test.go | 278 + interfaces/dbus/dbus.go | 52 + interfaces/dbus/dbus_test.go | 42 + interfaces/dbus/export_test.go | 35 + interfaces/dbus/spec.go | 132 + interfaces/dbus/spec_test.go | 105 + interfaces/dbus/template.go | 29 + interfaces/export_test.go | 76 + interfaces/ifacetest/backend.go | 78 + interfaces/ifacetest/backendtest.go | 195 + interfaces/ifacetest/ifacetest_test.go | 30 + interfaces/ifacetest/spec.go | 81 + interfaces/ifacetest/spec_test.go | 93 + interfaces/ifacetest/testiface.go | 400 + interfaces/ifacetest/testiface_test.go | 170 + interfaces/json.go | 118 + interfaces/json_test.go | 147 + interfaces/kmod/backend.go | 133 + interfaces/kmod/backend_test.go | 136 + interfaces/kmod/export_test.go | 24 + interfaces/kmod/kmod.go | 36 + interfaces/kmod/kmod_test.go | 56 + interfaces/kmod/spec.go | 104 + interfaces/kmod/spec_test.go | 129 + interfaces/mount/backend.go | 114 + interfaces/mount/backend_test.go | 164 + interfaces/mount/entry.go | 241 + interfaces/mount/entry_test.go | 190 + interfaces/mount/lock.go | 46 + interfaces/mount/lock_test.go | 53 + interfaces/mount/mountinfo.go | 160 + interfaces/mount/mountinfo_test.go | 159 + interfaces/mount/ns.go | 66 + interfaces/mount/ns_test.go | 144 + interfaces/mount/profile.go | 105 + interfaces/mount/profile_test.go | 133 + interfaces/mount/spec.go | 93 + interfaces/mount/spec_test.go | 93 + interfaces/naming.go | 34 + interfaces/naming_test.go | 38 + interfaces/policy/basedeclaration.go | 199 + interfaces/policy/basedeclaration_test.go | 807 ++ interfaces/policy/export_test.go | 25 + interfaces/policy/helpers.go | 219 + interfaces/policy/helpers_test.go | 54 + interfaces/policy/policy.go | 277 + interfaces/policy/policy_test.go | 1601 ++++ interfaces/repo.go | 989 +++ interfaces/repo_test.go | 1840 ++++ interfaces/seccomp/backend.go | 192 + interfaces/seccomp/backend_test.go | 342 + interfaces/seccomp/export_test.go | 38 + interfaces/seccomp/seccomp_test.go | 30 + interfaces/seccomp/spec.go | 137 + interfaces/seccomp/spec_test.go | 105 + interfaces/seccomp/template.go | 567 ++ interfaces/sorting.go | 161 + interfaces/sorting_test.go | 177 + interfaces/systemd/backend.go | 167 + interfaces/systemd/backend_test.go | 155 + interfaces/systemd/service.go | 58 + interfaces/systemd/service_test.go | 45 + interfaces/systemd/spec.go | 107 + interfaces/systemd/spec_test.go | 55 + interfaces/systemd/systemd_test.go | 30 + interfaces/udev/backend.go | 152 + interfaces/udev/backend_test.go | 424 + interfaces/udev/spec.go | 165 + interfaces/udev/spec_test.go | 134 + interfaces/udev/udev.go | 41 + interfaces/udev/udev_test.go | 87 + jsonutil/json.go | 40 + jsonutil/json_test.go | 65 + logger/export_test.go | 27 + logger/logger.go | 141 + logger/logger_test.go | 90 + mdlint.py | 36 + mkversion.sh | 73 + osutil/bootid.go | 40 + osutil/bootid_test.go | 36 + osutil/buildid.go | 101 + osutil/buildid_test.go | 88 + osutil/chattr.go | 76 + osutil/chattr_32.go | 27 + osutil/chattr_64.go | 28 + osutil/chdir.go | 38 + osutil/chdir_test.go | 59 + osutil/cmp.go | 91 + osutil/cmp_test.go | 101 + osutil/cp.go | 171 + osutil/cp_linux.go | 48 + osutil/cp_linux_test.go | 46 + osutil/cp_other.go | 31 + osutil/cp_test.go | 308 + osutil/digest.go | 46 + osutil/digest_test.go | 50 + osutil/env.go | 115 + osutil/env_test.go | 162 + osutil/exec.go | 283 + osutil/exec_test.go | 228 + osutil/exitcode.go | 37 + osutil/exitcode_test.go | 56 + osutil/export_test.go | 106 + osutil/flock.go | 73 + osutil/flock_test.go | 145 + osutil/fshelpers.go | 38 + osutil/fshelpers_test.go | 51 + osutil/group.go | 158 + osutil/io.go | 222 + osutil/io_test.go | 272 + osutil/mkdirallchown.go | 72 + osutil/mount.go | 58 + osutil/mount_test.go | 87 + osutil/osutil_test.go | 29 + osutil/outputerr.go | 39 + osutil/outputerr_test.go | 54 + osutil/stat.go | 89 + osutil/stat_test.go | 145 + osutil/syncdir.go | 144 + osutil/syncdir_test.go | 214 + osutil/user.go | 163 + osutil/user_test.go | 189 + osutil/winsize.go | 48 + overlord/assertstate/assertmgr.go | 138 + overlord/assertstate/assertstate.go | 386 + overlord/assertstate/assertstate_test.go | 1158 +++ overlord/assertstate/export_test.go | 25 + overlord/assertstate/helpers.go | 127 + overlord/auth/auth.go | 505 ++ overlord/auth/auth_test.go | 774 ++ overlord/backend.go | 45 + overlord/cmdstate/cmdmgr.go | 80 + overlord/cmdstate/cmdstate.go | 33 + overlord/cmdstate/cmdstate_test.go | 174 + overlord/cmdstate/export_test.go | 32 + overlord/configstate/config/helpers.go | 243 + overlord/configstate/config/helpers_test.go | 138 + overlord/configstate/config/transaction.go | 275 + .../configstate/config/transaction_test.go | 328 + overlord/configstate/configmgr.go | 88 + overlord/configstate/configstate.go | 97 + overlord/configstate/configstate_test.go | 202 + overlord/configstate/export_test.go | 22 + overlord/configstate/handler_test.go | 214 + overlord/configstate/hooks.go | 124 + overlord/devicestate/crypto.go | 80 + overlord/devicestate/devicemgr.go | 476 + overlord/devicestate/devicestate.go | 269 + overlord/devicestate/devicestate_test.go | 2172 +++++ overlord/devicestate/export_test.go | 126 + overlord/devicestate/firstboot.go | 291 + overlord/devicestate/firstboot_test.go | 969 ++ overlord/devicestate/handlers.go | 539 ++ overlord/export_test.go | 67 + overlord/hookstate/context.go | 250 + overlord/hookstate/context_test.go | 166 + overlord/hookstate/ctlcmd/ctlcmd.go | 115 + overlord/hookstate/ctlcmd/ctlcmd_test.go | 76 + overlord/hookstate/ctlcmd/export_test.go | 80 + overlord/hookstate/ctlcmd/get.go | 305 + overlord/hookstate/ctlcmd/get_test.go | 319 + overlord/hookstate/ctlcmd/helpers.go | 139 + overlord/hookstate/ctlcmd/restart.go | 56 + overlord/hookstate/ctlcmd/services_test.go | 422 + overlord/hookstate/ctlcmd/set.go | 165 + overlord/hookstate/ctlcmd/set_test.go | 281 + overlord/hookstate/ctlcmd/start.go | 56 + overlord/hookstate/ctlcmd/stop.go | 56 + overlord/hookstate/export_test.go | 46 + overlord/hookstate/hookmgr.go | 395 + overlord/hookstate/hooks.go | 111 + overlord/hookstate/hookstate.go | 39 + overlord/hookstate/hookstate_test.go | 930 ++ overlord/hookstate/hooktest/handler.go | 78 + overlord/hookstate/hooktest/handler_test.go | 91 + overlord/hookstate/repository.go | 74 + overlord/hookstate/repository_test.go | 69 + overlord/ifacestate/export_test.go | 46 + overlord/ifacestate/handlers.go | 554 ++ overlord/ifacestate/helpers.go | 450 + overlord/ifacestate/hooks.go | 74 + overlord/ifacestate/ifacemgr.go | 116 + overlord/ifacestate/ifacerepo/repo.go | 41 + overlord/ifacestate/ifacerepo/repo_test.go | 63 + overlord/ifacestate/ifacestate.go | 214 + overlord/ifacestate/ifacestate_test.go | 2088 +++++ overlord/ifacestate/implicit.go | 57 + overlord/ifacestate/implicit_test.go | 71 + overlord/managers_test.go | 1892 ++++ overlord/overlord.go | 438 + overlord/overlord_test.go | 728 ++ overlord/patch/export_test.go | 68 + overlord/patch/patch.go | 104 + overlord/patch/patch1.go | 123 + overlord/patch/patch1_test.go | 158 + overlord/patch/patch2.go | 165 + overlord/patch/patch2_test.go | 179 + overlord/patch/patch3.go | 63 + overlord/patch/patch3_test.go | 149 + overlord/patch/patch4.go | 322 + overlord/patch/patch4_test.go | 451 + overlord/patch/patch5.go | 84 + overlord/patch/patch6.go | 115 + overlord/patch/patch6_test.go | 209 + overlord/patch/patch_test.go | 192 + overlord/servicestate/servicestate.go | 111 + overlord/snapstate/aliasesv2.go | 696 ++ overlord/snapstate/aliasesv2_test.go | 1202 +++ overlord/snapstate/autorefresh.go | 271 + overlord/snapstate/autorefresh_test.go | 146 + overlord/snapstate/backend.go | 80 + overlord/snapstate/backend/aliases.go | 112 + overlord/snapstate/backend/aliases_test.go | 353 + overlord/snapstate/backend/backend.go | 51 + overlord/snapstate/backend/backend_test.go | 89 + overlord/snapstate/backend/copydata.go | 92 + overlord/snapstate/backend/copydata_test.go | 549 ++ overlord/snapstate/backend/export_test.go | 25 + overlord/snapstate/backend/link.go | 166 + overlord/snapstate/backend/link_test.go | 317 + overlord/snapstate/backend/mountns.go | 29 + overlord/snapstate/backend/mountunit.go | 93 + overlord/snapstate/backend/mountunit_test.go | 120 + overlord/snapstate/backend/setup.go | 103 + overlord/snapstate/backend/setup_test.go | 245 + overlord/snapstate/backend/snapdata.go | 244 + overlord/snapstate/backend/utils.go | 30 + overlord/snapstate/backend_test.go | 599 ++ overlord/snapstate/booted.go | 169 + overlord/snapstate/booted_test.go | 288 + overlord/snapstate/catalogrefresh.go | 102 + overlord/snapstate/catalogrefresh_test.go | 106 + overlord/snapstate/check_snap.go | 339 + overlord/snapstate/check_snap_test.go | 682 ++ overlord/snapstate/cookies.go | 168 + overlord/snapstate/cookies_test.go | 158 + overlord/snapstate/export_test.go | 155 + overlord/snapstate/flags.go | 67 + overlord/snapstate/handlers.go | 1574 ++++ overlord/snapstate/handlers_aliasesv2_test.go | 1951 ++++ overlord/snapstate/handlers_discard_test.go | 197 + overlord/snapstate/handlers_download_test.go | 198 + overlord/snapstate/handlers_link_test.go | 500 ++ overlord/snapstate/handlers_mount_test.go | 155 + overlord/snapstate/handlers_prepare_test.go | 91 + overlord/snapstate/progress.go | 99 + overlord/snapstate/progress_test.go | 58 + overlord/snapstate/readme.go | 65 + overlord/snapstate/readme_test.go | 80 + overlord/snapstate/refreshhints.go | 94 + overlord/snapstate/refreshhints_test.go | 85 + overlord/snapstate/snapmgr.go | 505 ++ overlord/snapstate/snapstate.go | 1771 ++++ overlord/snapstate/snapstate_test.go | 7900 +++++++++++++++++ overlord/snapstate/storehelpers.go | 81 + overlord/state/change.go | 610 ++ overlord/state/change_test.go | 725 ++ overlord/state/export_test.go | 46 + overlord/state/state.go | 462 + overlord/state/state_test.go | 937 ++ overlord/state/task.go | 477 + overlord/state/task_test.go | 497 ++ overlord/state/taskrunner.go | 433 + overlord/state/taskrunner_test.go | 800 ++ overlord/stateengine.go | 138 + overlord/stateengine_test.go | 123 + packaging/arch/PKGBUILD | 215 + packaging/arch/snapd.install | 43 + packaging/fedora-25 | 1 + packaging/fedora-26 | 1 + packaging/fedora-27 | 1 + packaging/fedora-rawhide | 1 + packaging/fedora/snap-mgmt.sh | 137 + packaging/fedora/snapd.spec | 1941 ++++ packaging/opensuse-42.1 | 1 + packaging/opensuse-42.2/permissions | 1 + packaging/opensuse-42.2/permissions.easy | 1 + packaging/opensuse-42.2/permissions.paranoid | 1 + packaging/opensuse-42.2/permissions.secure | 1 + packaging/opensuse-42.2/snapd-rpmlintrc | 1 + packaging/opensuse-42.2/snapd.changes | 158 + packaging/opensuse-42.2/snapd.spec | 307 + packaging/ubuntu-14.04/changelog | 4256 +++++++++ packaging/ubuntu-14.04/compat | 1 + packaging/ubuntu-14.04/control | 131 + packaging/ubuntu-14.04/copyright | 22 + .../golang-github-snapcore-snapd-dev.install | 1 + packaging/ubuntu-14.04/rules | 211 + .../ubuntu-14.04/snap-confine.maintscript | 1 + packaging/ubuntu-14.04/snap.mount.service | 16 + packaging/ubuntu-14.04/snapd.autoimport.udev | 1 + packaging/ubuntu-14.04/snapd.dirs | 12 + packaging/ubuntu-14.04/snapd.install | 30 + packaging/ubuntu-14.04/snapd.maintscript | 1 + packaging/ubuntu-14.04/snapd.manpages | 1 + packaging/ubuntu-14.04/snapd.postinst | 31 + packaging/ubuntu-14.04/snapd.postrm | 103 + packaging/ubuntu-14.04/snapd.prerm | 8 + packaging/ubuntu-14.04/source | 1 + packaging/ubuntu-14.04/tests/README.md | 1 + packaging/ubuntu-14.04/tests/control | 11 + packaging/ubuntu-14.04/tests/integrationtests | 41 + packaging/ubuntu-14.04/tests/testconfig.json | 1 + packaging/ubuntu-14.04/ubuntu-snappy-cli.dirs | 2 + packaging/ubuntu-16.04/changelog | 4292 +++++++++ packaging/ubuntu-16.04/compat | 1 + packaging/ubuntu-16.04/control | 125 + packaging/ubuntu-16.04/copyright | 22 + .../golang-github-snapcore-snapd-dev.install | 1 + packaging/ubuntu-16.04/rules | 209 + .../ubuntu-16.04/snap-confine.maintscript | 1 + packaging/ubuntu-16.04/snapd.autoimport.udev | 3 + packaging/ubuntu-16.04/snapd.dirs | 14 + packaging/ubuntu-16.04/snapd.install | 30 + packaging/ubuntu-16.04/snapd.maintscript | 5 + packaging/ubuntu-16.04/snapd.manpages | 1 + packaging/ubuntu-16.04/snapd.postinst | 14 + packaging/ubuntu-16.04/snapd.postrm | 106 + packaging/ubuntu-16.04/source/format | 1 + packaging/ubuntu-16.04/tests/README.md | 10 + packaging/ubuntu-16.04/tests/control | 9 + packaging/ubuntu-16.04/tests/integrationtests | 38 + packaging/ubuntu-16.04/tests/testconfig.json | 3 + packaging/ubuntu-16.04/ubuntu-snappy-cli.dirs | 2 + packaging/ubuntu-16.10 | 1 + packaging/ubuntu-17.04 | 1 + partition/androidboot.go | 77 + partition/androidboot_test.go | 65 + partition/androidbootenv/androidbootenv.go | 90 + .../androidbootenv/androidbootenv_test.go | 69 + partition/bootloader.go | 148 + partition/bootloader_test.go | 159 + partition/export_test.go | 40 + partition/grub.go | 83 + partition/grub_test.go | 126 + partition/grubenv/grubenv.go | 117 + partition/grubenv/grubenv_test.go | 94 + partition/uboot.go | 94 + partition/uboot_test.go | 134 + partition/ubootenv/env.go | 294 + partition/ubootenv/env_test.go | 298 + partition/ubootenv/export_test.go | 24 + partition/utils.go | 52 + partition/utils_test.go | 51 + po/am.po | 1803 ++++ po/bs.po | 1803 ++++ po/ca.po | 1803 ++++ po/cs.po | 1813 ++++ po/da.po | 1803 ++++ po/de.po | 1803 ++++ po/el.po | 1803 ++++ po/en_GB.po | 2206 +++++ po/es.po | 1805 ++++ po/fi.po | 1803 ++++ po/fr.po | 2237 +++++ po/gl.po | 1803 ++++ po/hr.po | 2161 +++++ po/ia.po | 1803 ++++ po/id.po | 1803 ++++ po/it.po | 1803 ++++ po/ja.po | 1803 ++++ po/lt.po | 1803 ++++ po/ms.po | 1803 ++++ po/nb.po | 1919 ++++ po/oc.po | 1803 ++++ po/pt.po | 1804 ++++ po/pt_BR.po | 1803 ++++ po/ru.po | 1804 ++++ po/sv.po | 1803 ++++ po/tr.po | 1803 ++++ po/ug.po | 1803 ++++ po/zh_CN.po | 1803 ++++ polkit/authority.go | 87 + polkit/pid_start_time.go | 68 + polkit/pid_start_time_test.go | 71 + progress/ansimeter.go | 207 + progress/ansimeter_test.go | 263 + progress/example_test.go | 112 + progress/export_test.go | 103 + progress/progress.go | 105 + progress/progress_test.go | 65 + progress/progresstest/progresstest.go | 68 + progress/quantity.go | 202 + release/apparmor.go | 117 + release/apparmor_test.go | 75 + release/export_linux_test.go | 25 + release/export_test.go | 46 + release/release.go | 148 + release/release_test.go | 156 + release/uname_linux.go | 87 + release/uname_linux_test.go | 73 + run-checks | 257 + snap/broken.go | 99 + snap/broken_test.go | 115 + snap/container.go | 94 + snap/container_test.go | 54 + snap/epoch.go | 297 + snap/epoch_test.go | 231 + snap/errors.go | 50 + snap/export_test.go | 35 + snap/gadget.go | 132 + snap/gadget_test.go | 221 + snap/hooktypes.go | 64 + snap/implicit.go | 83 + snap/implicit_test.go | 28 + snap/info.go | 718 ++ snap/info_snap_yaml.go | 564 ++ snap/info_snap_yaml_test.go | 1561 ++++ snap/info_test.go | 830 ++ snap/pack/export_test.go | 26 + snap/pack/pack.go | 268 + snap/pack/pack_test.go | 286 + snap/restartcond.go | 75 + snap/restartcond_test.go | 51 + snap/revision.go | 123 + snap/revision_test.go | 199 + snap/seed_yaml.go | 90 + snap/seed_yaml_test.go | 83 + snap/snapdir/snapdir.go | 79 + snap/snapdir/snapdir_test.go | 82 + snap/snapenv/snapenv.go | 200 + snap/snapenv/snapenv_test.go | 198 + snap/snaptest/snaptest.go | 145 + snap/snaptest/snaptest_test.go | 107 + snap/squashfs/squashfs.go | 160 + snap/squashfs/squashfs_test.go | 170 + snap/types.go | 120 + snap/types_test.go | 220 + snap/validate.go | 387 + snap/validate_test.go | 594 ++ spdx/licenses.go | 501 ++ spdx/parser.go | 157 + spdx/parser_test.go | 77 + spdx/scanner.go | 69 + spdx/scanner_test.go | 49 + spdx/validate.go | 34 + spread.yaml | 544 ++ store/auth.go | 319 + store/auth_test.go | 445 + store/cache.go | 153 + store/cache_test.go | 113 + store/details.go | 95 + store/errors.go | 112 + store/export_test.go | 39 + store/store.go | 1992 +++++ store/store_test.go | 5191 +++++++++++ store/storetest/storetest.go | 86 + store/userinfo.go | 86 + store/userinfo_test.go | 115 + strutil/map.go | 118 + strutil/map_test.go | 85 + strutil/strutil.go | 129 + strutil/strutil_test.go | 129 + strutil/version.go | 186 + strutil/version_test.go | 124 + systemd/escape.go | 66 + systemd/escape_test.go | 37 + systemd/export_test.go | 52 + systemd/sdnotify.go | 59 + systemd/sdnotify_test.go | 87 + systemd/systemd.go | 459 + systemd/systemd_test.go | 602 ++ tests/completion/data/files/a/a_thing.txt | 0 tests/completion/data/files/b/b_thing.txt | 0 tests/completion/data/files/b/c/b_c_thing.txt | 0 tests/completion/data/files/d/d_thing.txt | 0 tests/completion/data/files/thing.txt | 0 tests/completion/data/hosts.txt | 1 + .../data/twisted/.just a hidden file | 0 .../this is a file with spaces in it.doc | 0 .../completion/data/twisted/this isn't.innit | 0 tests/completion/dirs.complete | 5 + tests/completion/dirs.sh | 1 + tests/completion/dirs.vars | 8 + tests/completion/files.complete | 4 + tests/completion/files.sh | 1 + tests/completion/files.vars | 7 + tests/completion/func.complete | 8 + tests/completion/func.sh | 0 tests/completion/func.vars | 7 + tests/completion/funky.complete | 4 + tests/completion/funky.sh | 0 tests/completion/funky.vars | 7 + tests/completion/funkyfunc.complete | 12 + tests/completion/funkyfunc.sh | 0 tests/completion/funkyfunc.vars | 7 + tests/completion/hosts.complete | 4 + tests/completion/hosts.sh | 1 + tests/completion/hosts.vars | 7 + tests/completion/hosts_n_dirs.complete | 4 + tests/completion/hosts_n_dirs.sh | 2 + tests/completion/hosts_n_dirs.vars | 7 + tests/completion/indirect/task.exp | 21 + tests/completion/indirect/task.yaml | 27 + tests/completion/lib.exp0 | 70 + tests/completion/plain.complete | 4 + tests/completion/plain.sh | 0 tests/completion/plain.vars | 7 + tests/completion/plain_plusdirs.complete | 4 + tests/completion/plain_plusdirs.sh | 1 + tests/completion/plain_plusdirs.vars | 7 + tests/completion/simple/task.exp | 16 + tests/completion/simple/task.yaml | 7 + tests/completion/snippets/task.exp | 17 + tests/completion/snippets/task.yaml | 22 + tests/completion/twisted.complete | 4 + tests/completion/twisted.sh | 1 + tests/completion/twisted.vars | 7 + tests/external-backend.md | 37 + tests/lib/assertions/auto-import.assert | 28 + .../developer1-my-classic-w-gadget.model | 20 + .../assertions/developer1-my-classic.model | 19 + .../assertions/developer1-pc-w-config.model | 24 + tests/lib/assertions/developer1-pc.model | 21 + tests/lib/assertions/developer1.account | 19 + tests/lib/assertions/developer1.account-key | 30 + tests/lib/assertions/fake.store | 19 + tests/lib/assertions/nested-amd64.model | 21 + tests/lib/assertions/nested-i386.model | 21 + tests/lib/assertions/pc-production.model | 21 + tests/lib/assertions/pc-staging.model | 21 + tests/lib/assertions/pi2.model | 21 + .../assertions/testrootorg-store.account-key | 30 + tests/lib/boot.sh | 35 + tests/lib/cache/README.txt | 3 + tests/lib/changes.sh | 8 + tests/lib/dbus.sh | 35 + tests/lib/dirs.sh | 13 + tests/lib/external/prepare-ssh.sh | 15 + tests/lib/fakedevicesvc/main.go | 135 + .../cmd/fakestore/cmd_make_refreshable.go | 39 + .../cmd/fakestore/cmd_new_snap_decl.go | 63 + .../cmd/fakestore/cmd_new_snap_rev.go | 63 + tests/lib/fakestore/cmd/fakestore/cmd_run.go | 70 + tests/lib/fakestore/cmd/fakestore/main.go | 51 + tests/lib/fakestore/refresh/refresh.go | 258 + tests/lib/fakestore/refresh/snap_asserts.go | 95 + tests/lib/fakestore/store/store.go | 565 ++ tests/lib/fakestore/store/store_test.go | 392 + tests/lib/files.sh | 15 + tests/lib/list-interfaces.go | 10 + tests/lib/mkpinentry.sh | 21 + tests/lib/names.sh | 8 + tests/lib/nested.sh | 68 + tests/lib/network.sh | 5 + tests/lib/os-release.16 | 7 + tests/lib/pinentry-fake.sh | 20 + tests/lib/pkgdb.sh | 489 + tests/lib/prepare-restore.sh | 363 + tests/lib/prepare.sh | 534 ++ tests/lib/quiet.sh | 30 + tests/lib/ramdisk.sh | 7 + tests/lib/reset.sh | 111 + tests/lib/snaps.sh | 77 + .../account-control-consumer/bin/chpasswd | 3 + .../account-control-consumer/bin/deluser | 3 + .../account-control-consumer/bin/useradd | 3 + .../account-control-consumer/meta/snap.yaml | 15 + tests/lib/snaps/aliases/bin/cmd1 | 2 + tests/lib/snaps/aliases/bin/cmd2 | 2 + tests/lib/snaps/aliases/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/aliases/meta/snap.yaml | 9 + tests/lib/snaps/basic-desktop/bin/echo | 3 + .../snaps/basic-desktop/meta/gui/echo.desktop | 10 + .../lib/snaps/basic-desktop/meta/gui/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/basic-desktop/meta/snap.yaml | 5 + .../snaps/basic-hooks/meta/hooks/configure | 7 + .../snaps/basic-hooks/meta/hooks/invalid-hook | 3 + tests/lib/snaps/basic-hooks/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/basic-hooks/meta/snap.yaml | 6 + .../meta/hooks/connect-plug-foo | 57 + .../meta/hooks/prepare-plug-foo | 36 + .../basic-iface-hooks-consumer/meta/icon.png | Bin 0 -> 3371 bytes .../basic-iface-hooks-consumer/meta/snap.yaml | 7 + .../meta/hooks/connect-slot-bar | 48 + .../meta/hooks/prepare-slot-bar | 36 + .../basic-iface-hooks-producer/meta/icon.png | Bin 0 -> 3371 bytes .../basic-iface-hooks-producer/meta/snap.yaml | 8 + tests/lib/snaps/basic-run/bin/echo | 3 + tests/lib/snaps/basic-run/meta/snap.yaml | 6 + tests/lib/snaps/basic/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/basic/meta/snap.yaml | 4 + .../snaps/browser-support-consumer/bin/cmd | 2 + .../meta/snap.yaml.in | 10 + .../lib/snaps/classic-gadget/meta/gadget.yaml | 1 + .../classic-gadget/meta/hooks/prepare-device | 2 + tests/lib/snaps/classic-gadget/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/classic-gadget/meta/snap.yaml | 4 + .../config-versions-v2/meta/hooks/configure | 2 + .../snaps/config-versions-v2/meta/icon.png | Bin 0 -> 3371 bytes .../snaps/config-versions-v2/meta/snap.yaml | 2 + .../config-versions/meta/hooks/configure | 2 + tests/lib/snaps/config-versions/meta/icon.png | Bin 0 -> 3371 bytes .../lib/snaps/config-versions/meta/snap.yaml | 2 + tests/lib/snaps/data-writer/bin/write-data | 20 + tests/lib/snaps/data-writer/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/data-writer/meta/snap.yaml | 9 + .../failing-config-hooks/meta/hooks/configure | 4 + .../snaps/failing-config-hooks/meta/icon.png | Bin 0 -> 3371 bytes .../snaps/failing-config-hooks/meta/snap.yaml | 2 + .../firewall-control-consumer/bin/consumer | 3 + .../firewall-control-consumer/meta/snap.yaml | 12 + tests/lib/snaps/generic-consumer/bin/cmd | 5 + .../snaps/generic-consumer/meta/snap.yaml.in | 6 + tests/lib/snaps/gpio-consumer/bin/read | 3 + tests/lib/snaps/gpio-consumer/meta/snap.yaml | 9 + .../hardware-observe-consumer/bin/consumer | 5 + .../hardware-observe-consumer/meta/snap.yaml | 9 + tests/lib/snaps/home-consumer/bin/reader | 14 + tests/lib/snaps/home-consumer/bin/writer | 15 + tests/lib/snaps/home-consumer/meta/snap.yaml | 12 + tests/lib/snaps/iio-consumer/bin/read | 2 + tests/lib/snaps/iio-consumer/bin/write | 2 + tests/lib/snaps/iio-consumer/meta/snap.yaml | 12 + .../lib/snaps/locale-control-consumer/bin/get | 14 + .../lib/snaps/locale-control-consumer/bin/set | 23 + .../locale-control-consumer/meta/snap.yaml | 12 + tests/lib/snaps/log-observe-consumer/bin/cmd | 6 + .../snaps/log-observe-consumer/bin/consumer | 15 + .../snaps/log-observe-consumer/meta/snap.yaml | 12 + .../snaps/modem-manager-consumer/bin/consumer | 3 + .../modem-manager-consumer/meta/snap.yaml | 9 + .../snaps/mount-observe-consumer/bin/consumer | 12 + .../mount-observe-consumer/meta/snap.yaml | 9 + .../snaps/network-bind-consumer/bin/consumer | 22 + .../network-bind-consumer/meta/snap.yaml | 10 + tests/lib/snaps/network-consumer/bin/consumer | 21 + .../lib/snaps/network-consumer/meta/snap.yaml | 9 + .../snaps/network-control-consumer/bin/cmd | 6 + .../network-control-consumer/meta/snap.yaml | 9 + .../network-observe-consumer/bin/consumer | 10 + .../network-observe-consumer/meta/snap.yaml | 9 + .../snaps/process-control-consumer/bin/signal | 6 + .../process-control-consumer/meta/snap.yaml | 9 + .../bin/consumer | 5 + .../meta/snap.yaml | 9 + .../lib/snaps/snap-hooks/meta/hooks/configure | 3 + tests/lib/snaps/snap-hooks/meta/hooks/install | 3 + .../snaps/snap-hooks/meta/hooks/post-refresh | 3 + .../snaps/snap-hooks/meta/hooks/pre-refresh | 3 + tests/lib/snaps/snap-hooks/meta/hooks/remove | 5 + tests/lib/snaps/snap-hooks/meta/snap.yaml | 8 + .../meta/hooks/install | 4 + .../snap-install-hook-broken/meta/snap.yaml | 5 + .../snaps/snapctl-from-snap/bin/snapctl-get | 2 + .../snaps/snapctl-from-snap/bin/snapctl-set | 2 + .../snapctl-from-snap/meta/hooks/configure | 3 + .../snaps/snapctl-from-snap/meta/snap.yaml | 7 + .../snapctl-hooks-v2/meta/hooks/configure | 13 + .../lib/snaps/snapctl-hooks-v2/meta/icon.png | Bin 0 -> 3371 bytes .../lib/snaps/snapctl-hooks-v2/meta/snap.yaml | 2 + .../snaps/snapctl-hooks/meta/hooks/configure | 111 + tests/lib/snaps/snapctl-hooks/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/snapctl-hooks/meta/snap.yaml | 2 + tests/lib/snaps/socket-activation/bin/sleep | 3 + .../snaps/socket-activation/meta/snap.yaml | 12 + .../lib/snaps/test-classic-cgroup/bin/read-fb | 3 + .../snaps/test-classic-cgroup/bin/read-kmsg | 3 + .../snaps/test-classic-cgroup/meta/snap.yaml | 12 + .../lib/snaps/test-devmode-cgroup/bin/read-fb | 3 + .../snaps/test-devmode-cgroup/bin/read-kmsg | 3 + .../snaps/test-devmode-cgroup/meta/snap.yaml | 14 + .../test-snapd-auto-aliases/bin/wellknown1 | 2 + .../test-snapd-auto-aliases/bin/wellknown2 | 2 + .../test-snapd-auto-aliases/meta/icon.png | Bin 0 -> 3371 bytes .../test-snapd-auto-aliases/meta/snap.yaml | 9 + .../test-snapd-autopilot-consumer/consumer | 22 + .../test-snapd-autopilot-consumer/provider.py | 30 + .../snapcraft.yaml | 28 + .../test-snapd-autopilot-consumer/wrapper | 3 + tests/lib/snaps/test-snapd-base-bare/Makefile | 13 + .../snaps/test-snapd-base-bare/snapcraft.yaml | 12 + .../lib/snaps/test-snapd-base/meta/snap.yaml | 4 + tests/lib/snaps/test-snapd-base/random-file | 1 + .../test-snapd-busybox-static/snapcraft.yaml | 17 + .../test-snapd-check-fs-access/bin/read-dir | 14 + .../test-snapd-check-fs-access/bin/read-file | 14 + .../test-snapd-check-fs-access/bin/write-dir | 20 + .../test-snapd-check-fs-access/bin/write-file | 14 + .../test-snapd-check-fs-access/meta/snap.yaml | 18 + .../bin/classic-confinement | 4 + .../bin/recurse | 6 + .../meta/icon.png | Bin 0 -> 3371 bytes .../meta/snap.yaml | 8 + .../bin/test-snapd-complexion | 7 + .../test-snapd-complexion/meta/snap.yaml | 11 + .../test-snapd-complexion.bash-completer | 31 + .../meta/snap.yaml | 22 + .../meta/snap.yaml | 16 + .../source/canary | 1 + .../bin/content-plug | 11 + .../import/.placeholder | 0 .../meta/snap.yaml | 11 + .../test-snapd-content-plug/bin/content-plug | 11 + .../import/.placeholder | 0 .../test-snapd-content-plug/meta/snap.yaml | 12 + .../meta/snap.yaml | 7 + .../shared-content | 3 + .../test-snapd-content-slot/meta/snap.yaml | 8 + .../test-snapd-content-slot/shared-content | 3 + .../test-snapd-control-consumer/bin/install | 22 + .../test-snapd-control-consumer/bin/list | 19 + .../meta/snap.yaml | 26 + .../snapcraft.yaml | 13 + .../test-snapd-dbus-consumer/consumer.py | 10 + .../test-snapd-dbus-consumer/snapcraft.yaml | 25 + .../test-snapd-dbus-provider/provider.py | 24 + .../test-snapd-dbus-provider/snapcraft.yaml | 23 + .../snaps/test-snapd-dbus-provider/wrapper | 3 + .../snaps/test-snapd-desktop/bin/check-dirs | 5 + .../snaps/test-snapd-desktop/bin/check-files | 5 + .../snaps/test-snapd-desktop/meta/snap.yaml | 12 + .../snaps/test-snapd-devmode/meta/snap.yaml | 8 + tests/lib/snaps/test-snapd-devpts/bin/openpty | 18 + tests/lib/snaps/test-snapd-devpts/bin/useptmx | 20 + .../snaps/test-snapd-devpts/meta/snap.yaml | 9 + .../lib/snaps/test-snapd-framebuffer/bin/read | 3 + .../snaps/test-snapd-framebuffer/bin/write | 3 + .../test-snapd-framebuffer/meta/snap.yaml | 12 + .../snaps/test-snapd-fuse-consumer/Makefile | 9 + .../test-snapd-fuse-consumer/snapcraft.yaml | 18 + .../lib/snaps/test-snapd-go-webserver/main.go | 25 + .../test-snapd-go-webserver/snapcraft.yaml | 19 + .../bin/check | 4 + .../meta/snap.yaml | 10 + .../snapcraft.yaml | 24 + .../bin/machine-down | 3 + .../bin/machine-up | 3 + .../snapcraft.yaml | 49 + .../vm/ping-unikernel.xml | 22 + .../snaps/test-snapd-multi-service/bin/start | 6 + .../test-snapd-multi-service/meta/snap.yaml | 9 + .../bin/ovs-vsctl | 3 + .../snapcraft.yaml | 21 + .../bin/secret-tool | 3 + .../snapcraft.yaml | 21 + .../bin/head-mem | 3 + .../meta/snap.yaml | 9 + .../test-snapd-policy-app-consumer/bin/run | 10 + .../meta/gui/test-desktop.desktop | 6 + .../meta/snap.yaml | 321 + .../bin/run | 10 + .../meta/snap.yaml | 95 + .../bin/run | 10 + .../meta/snap.yaml | 131 + .../snaps/test-snapd-private/meta/snap.yaml | 4 + .../test-snapd-python-webserver/index.html | 43 + .../test-snapd-python-webserver/server.py | 46 + .../snapcraft.yaml | 21 + .../test-snapd-requires-base/meta/snap.yaml | 4 + .../test-snapd-service-try-v1/bin/service | 3 + .../test-snapd-service-try-v1/meta/snap.yaml | 5 + .../test-snapd-service-try-v2/bin/service | 3 + .../test-snapd-service-try-v2/meta/snap.yaml | 6 + .../snaps/test-snapd-service-v1-good/bin/good | 3 + .../test-snapd-service-v1-good/meta/snap.yaml | 7 + .../snaps/test-snapd-service-v2-bad/bin/bad | 4 + .../test-snapd-service-v2-bad/meta/snap.yaml | 7 + tests/lib/snaps/test-snapd-service/bin/reload | 3 + tests/lib/snaps/test-snapd-service/bin/start | 6 + .../snaps/test-snapd-service/bin/start-other | 6 + .../test-snapd-service/meta/hooks/configure | 19 + .../snaps/test-snapd-service/meta/snap.yaml | 10 + tests/lib/snaps/test-snapd-sh/meta/snap.yaml | 9 + .../consumer.py | 11 + .../dbus-introspect.py | 10 + .../snapcraft.yaml | 25 + tests/lib/snaps/test-snapd-tools/bin/block | 14 + tests/lib/snaps/test-snapd-tools/bin/cat | 3 + tests/lib/snaps/test-snapd-tools/bin/cmd | 6 + tests/lib/snaps/test-snapd-tools/bin/echo | 3 + tests/lib/snaps/test-snapd-tools/bin/env | 3 + tests/lib/snaps/test-snapd-tools/bin/fail | 3 + tests/lib/snaps/test-snapd-tools/bin/head | 3 + tests/lib/snaps/test-snapd-tools/bin/sh | 13 + tests/lib/snaps/test-snapd-tools/bin/success | 3 + .../lib/snaps/test-snapd-tools/meta/icon.png | Bin 0 -> 3371 bytes .../lib/snaps/test-snapd-tools/meta/snap.yaml | 27 + .../lib/snaps/test-snapd-tuntap/bin/tuntap.py | 79 + .../snaps/test-snapd-tuntap/meta/snap.yaml | 10 + tests/lib/snaps/test-snapd-uhid/Makefile | 5 + .../lib/snaps/test-snapd-uhid/snapcraft.yaml | 17 + tests/lib/snaps/test-snapd-uhid/uhid-test.c | 190 + .../meta/snap.yaml | 10 + .../snapcraft.yaml | 14 + .../meta/hooks/configure | 2 + .../test-snapd-with-configure/meta/snap.yaml | 4 + .../lib/snaps/test-strict-cgroup/bin/read-fb | 3 + .../snaps/test-strict-cgroup/bin/read-kmsg | 3 + .../snaps/test-strict-cgroup/meta/snap.yaml | 14 + .../lib/snaps/time-control-consumer/bin/read | 2 + .../time-control-consumer/bin/timedatectl | 2 + .../lib/snaps/time-control-consumer/bin/write | 2 + .../time-control-consumer/meta/snap.yaml | 15 + tests/lib/store.sh | 93 + tests/lib/strings.sh | 5 + tests/lib/systemd-escape/main.go | 52 + tests/lib/systemd.sh | 38 + tests/main/abort/task.yaml | 42 + tests/main/ack/alice.account | 19 + tests/main/ack/alice.account-key | 31 + tests/main/ack/bob.assertions | 51 + tests/main/ack/task.yaml | 31 + tests/main/alias/task.yaml | 55 + tests/main/auth-errors/task.yaml | 26 + tests/main/auto-aliases/task.yaml | 37 + tests/main/auto-refresh/task.yaml | 49 + tests/main/base-snaps/task.yaml | 41 + tests/main/canonical-livepatch/task.yaml | 19 + tests/main/catalog-update/task.yaml | 16 + tests/main/cgroup-freezer/task.yaml | 42 + tests/main/change-errors/task.yaml | 10 + tests/main/chattr/task.yaml | 19 + tests/main/chattr/toggle.go | 51 + .../task.yaml | 38 + tests/main/classic-confinement/task.yaml | 42 + .../main/classic-custom-device-reg/task.yaml | 75 + tests/main/classic-firstboot/task.yaml | 72 + .../task.yaml | 67 + .../task.yaml | 72 + .../classic-ubuntu-core-transition/task.yaml | 122 + tests/main/cmdline/task.yaml | 10 + tests/main/completion/abort.exp | 7 + tests/main/completion/ack.exp | 8 + tests/main/completion/alias.exp | 13 + tests/main/completion/buy.exp | 8 + tests/main/completion/change.exp | 7 + tests/main/completion/delete-key.exp | 6 + tests/main/completion/disable.exp | 6 + tests/main/completion/download.exp | 7 + tests/main/completion/enable.exp | 6 + tests/main/completion/export-key.exp | 6 + tests/main/completion/get.exp | 6 + tests/main/completion/info.exp | 11 + tests/main/completion/install.exp | 22 + tests/main/completion/key.exp0 | 17 + tests/main/completion/lib.exp0 | 1 + tests/main/completion/list.exp | 7 + tests/main/completion/refresh.exp | 6 + tests/main/completion/remove.exp | 6 + tests/main/completion/revert.exp | 6 + tests/main/completion/set.exp | 6 + tests/main/completion/sign-build.exp | 6 + tests/main/completion/sign.exp | 6 + tests/main/completion/task.yaml | 45 + tests/main/completion/toplevel.exp | 23 + tests/main/completion/try.exp | 6 + tests/main/completion/watch.exp | 7 + tests/main/config-versions/task.yaml | 64 + tests/main/confinement-classic/task.yaml | 35 + .../test-snapd-hello-classic/Makefile | 82 + .../test-snapd-hello-classic.c | 12 + tests/main/core-snap-not-test-test/task.yaml | 5 + .../main/core-snap-refresh-on-core/task.yaml | 122 + tests/main/core-snap-refresh/task.yaml | 48 + tests/main/create-key/passphrase_mismatch.exp | 17 + tests/main/create-key/successful_default.exp | 48 + .../create-key/successful_non_default.exp | 48 + tests/main/create-key/task.yaml | 19 + tests/main/create-user/task.yaml | 32 + tests/main/debs-have-built-using/task.yaml | 12 + tests/main/debug-confinement/task.yaml | 12 + .../main/dirs-not-shared-with-host/task.yaml | 30 + tests/main/econnreset/task.yaml | 42 + .../main/enable-disable-units-gpio/task.yaml | 87 + tests/main/enable-disable/task.yaml | 43 + tests/main/failover/task.yaml | 113 + tests/main/fakestore-install/task.yaml | 30 + tests/main/find-private/successful_login.exp | 12 + tests/main/find-private/task.yaml | 48 + tests/main/generic-classic-reg/task.yaml | 26 + tests/main/help/task.yaml | 13 + tests/main/i18n/task.yaml | 26 + tests/main/install-cache/task.yaml | 13 + tests/main/install-errors/task.yaml | 63 + .../install-refresh-remove-hooks/task.yaml | 76 + tests/main/install-remove-multi/task.yaml | 12 + tests/main/install-sideload/task.yaml | 77 + tests/main/install-snaps/task.yaml | 110 + .../main/install-socket-activation/task.yaml | 17 + tests/main/install-store-laaaarge/task.yaml | 16 + tests/main/install-store/task.yaml | 41 + .../main/interfaces-account-control/task.yaml | 30 + tests/main/interfaces-alsa/task.yaml | 100 + .../task.yaml | 85 + tests/main/interfaces-avahi-observe/task.yaml | 52 + .../interfaces-bluetooth-control/task.yaml | 60 + tests/main/interfaces-bluez/task.yaml | 19 + .../main/interfaces-browser-support/task.yaml | 161 + tests/main/interfaces-cli/task.yaml | 28 + .../task.yaml | 41 + .../task.yaml | 103 + tests/main/interfaces-content/task.yaml | 48 + tests/main/interfaces-cups-control/task.yaml | 81 + tests/main/interfaces-dbus/task.yaml | 64 + tests/main/interfaces-desktop/task.yaml | 61 + .../interfaces-firewall-control/task.yaml | 89 + tests/main/interfaces-framebuffer/task.yaml | 48 + tests/main/interfaces-fuse_support/task.yaml | 61 + .../interfaces-hardware-observe/task.yaml | 49 + .../task.yaml | 55 + tests/main/interfaces-home/task.yaml | 155 + tests/main/interfaces-hooks/task.yaml | 17 + tests/main/interfaces-iio/task.yaml | 48 + .../task.yaml | 126 + tests/main/interfaces-libvirt/task.yaml | 78 + .../main/interfaces-locale-control/task.yaml | 106 + tests/main/interfaces-log-observe/task.yaml | 68 + tests/main/interfaces-many/task.yaml | 108 + tests/main/interfaces-mount-observe/task.yaml | 67 + tests/main/interfaces-network-bind/task.yaml | 77 + .../task.yaml | 35 + .../task.yaml | 43 + .../main/interfaces-network-control/task.yaml | 159 + .../main/interfaces-network-manager/task.yaml | 56 + .../main/interfaces-network-observe/task.yaml | 68 + .../task.yaml | 60 + .../task.yaml | 58 + tests/main/interfaces-network/task.yaml | 79 + tests/main/interfaces-openvswitch/task.yaml | 118 + .../task.yaml | 52 + .../task.yaml | 52 + .../main/interfaces-process-control/task.yaml | 61 + .../task.yaml | 49 + .../task.yaml | 94 + tests/main/interfaces-snapd-control/task.yaml | 55 + .../main/interfaces-system-observe/task.yaml | 71 + tests/main/interfaces-time-control/task.yaml | 38 + tests/main/interfaces-udev/task.yaml | 33 + tests/main/interfaces-uhid/task.yaml | 49 + .../main/interfaces-upower-observe/task.yaml | 74 + tests/main/interfaces-wayland/task.yaml | 65 + tests/main/known-remote/task.yaml | 8 + tests/main/known/task.yaml | 15 + tests/main/listing/task.yaml | 41 + tests/main/local-install-w-metadata/digest.go | 17 + tests/main/local-install-w-metadata/task.yaml | 23 + tests/main/login/missing_email_error.exp | 9 + tests/main/login/successful_login.exp | 13 + tests/main/login/task.yaml | 33 + tests/main/login/unsuccessful_login.exp | 14 + tests/main/lxd/task.yaml | 92 + tests/main/manpages/task.yaml | 17 + tests/main/media-sharing/task.yaml | 21 + tests/main/nfs-support/task.yaml | 123 + tests/main/op-install-failed-undone/task.yaml | 53 + tests/main/op-remove-retry/task.yaml | 42 + tests/main/op-remove/task.yaml | 32 + tests/main/postrm-purge/task.yaml | 37 + tests/main/prefer/task.yaml | 34 + tests/main/prepare-image-grub/task.yaml | 84 + tests/main/prepare-image-uboot/task.yaml | 69 + tests/main/refresh-all-undo/task.yaml | 80 + tests/main/refresh-all/task.yaml | 62 + tests/main/refresh-delta-from-core/task.yaml | 27 + tests/main/refresh-delta/task.yaml | 26 + tests/main/refresh-devmode/task.yaml | 77 + tests/main/refresh-undo/task.yaml | 48 + tests/main/refresh/task.yaml | 115 + .../regression-home-snap-root-owned/task.yaml | 34 + tests/main/remove-errors/task.yaml | 15 + tests/main/revert-devmode/task.yaml | 83 + tests/main/revert-sideload/task.yaml | 20 + tests/main/revert/task.yaml | 100 + tests/main/searching/task.yaml | 41 + tests/main/security-apparmor/task.yaml | 22 + .../security-device-cgroups-classic/task.yaml | 37 + .../security-device-cgroups-devmode/task.yaml | 42 + .../task.yaml | 45 + .../task.yaml | 55 + .../security-device-cgroups-strict/task.yaml | 44 + tests/main/security-device-cgroups/task.yaml | 121 + tests/main/security-devpts/task.yaml | 35 + tests/main/security-private-tmp/task.yaml | 49 + .../main/security-private-tmp/tmp-create.exp | 15 + tests/main/security-profiles/task.yaml | 31 + tests/main/security-setuid-root/task.yaml | 41 + tests/main/server-snap/task.yaml | 36 + tests/main/set-proxy-store/task.yaml | 74 + .../snap-auto-import-asserts-spools/task.yaml | 54 + tests/main/snap-auto-import-asserts/task.yaml | 38 + tests/main/snap-auto-mount/task.yaml | 56 + tests/main/snap-confine-from-core/task.yaml | 28 + tests/main/snap-confine-privs/task.yaml | 63 + tests/main/snap-confine-privs/uids-and-gids.c | 40 + tests/main/snap-confine/task.yaml | 41 + tests/main/snap-connect/task.yaml | 56 + .../snap-debug-get-base-declaration/task.yaml | 9 + tests/main/snap-discard-ns/task.yaml | 36 + tests/main/snap-disconnect/task.yaml | 42 + tests/main/snap-download/task.yaml | 35 + tests/main/snap-env/task.yaml | 54 + tests/main/snap-get/task.yaml | 106 + tests/main/snap-info/check.py | 141 + tests/main/snap-info/task.yaml | 29 + .../snap-interface-core-support.yaml | 6 + tests/main/snap-interface/task.yaml | 11 + .../main/snap-multi-service-failing/task.yaml | 10 + tests/main/snap-on-non-shared-root/task.yaml | 34 + tests/main/snap-readme/task.yaml | 10 + tests/main/snap-remove-not-mounted/task.yaml | 15 + tests/main/snap-repair/task.yaml | 22 + tests/main/snap-run-alias/task.yaml | 39 + tests/main/snap-run-hook/task.yaml | 52 + tests/main/snap-run-symlink-error/task.yaml | 21 + tests/main/snap-run-symlink/task.yaml | 34 + .../main/snap-run-userdata-current/task.yaml | 40 + tests/main/snap-run/task.yaml | 9 + tests/main/snap-seccomp/task.yaml | 138 + tests/main/snap-service/task.yaml | 18 + tests/main/snap-set-core-w-no-core/task.yaml | 16 + tests/main/snap-set/task.yaml | 59 + tests/main/snap-sign/create-key.exp | 17 + tests/main/snap-sign/sign-model.exp | 20 + tests/main/snap-sign/task.yaml | 42 + tests/main/snap-switch/task.yaml | 8 + tests/main/snap-update-ns/task.yaml | 77 + tests/main/snap-userd-reexec/task.yaml | 17 + tests/main/snap-userd/task.yaml | 65 + tests/main/snapctl-configure-core/task.yaml | 67 + tests/main/snapctl-from-snap/task.yaml | 90 + tests/main/snapctl-services/task.yaml | 78 + tests/main/snapctl/task.yaml | 27 + tests/main/snapd-notify/task.yaml | 17 + tests/main/snapd-reexec/task.yaml | 94 + tests/main/static/task.yaml | 5 + tests/main/systemd-service/task.yaml | 18 + tests/main/try-non-fatal/task.yaml | 18 + tests/main/try-snap-goes-away/task.yaml | 44 + tests/main/try-snap-is-optional/task.yaml | 9 + tests/main/try-twice-with-daemon/task.yaml | 35 + tests/main/try/task.yaml | 93 + tests/main/ubuntu-core-apt/task.yaml | 9 + tests/main/ubuntu-core-classic/task.yaml | 49 + tests/main/ubuntu-core-create-user/task.yaml | 38 + .../manip_seed.py | 21 + .../prepare-device | 5 + .../task.yaml | 81 + .../manip_seed.py | 21 + .../prepare-device | 2 + .../ubuntu-core-custom-device-reg/task.yaml | 79 + tests/main/ubuntu-core-device-reg/task.yaml | 28 + tests/main/ubuntu-core-fan/task.yaml | 16 + .../manip_seed.py | 27 + .../task.yaml | 102 + tests/main/ubuntu-core-grub/task.yaml | 12 + tests/main/ubuntu-core-os-release/task.yaml | 5 + tests/main/ubuntu-core-reboot/task.yaml | 37 + tests/main/ubuntu-core-services/task.yaml | 17 + tests/main/ubuntu-core-uboot/task.yaml | 12 + tests/main/ubuntu-core-upgrade/task.yaml | 88 + .../main/ubuntu-core-writablepaths/task.yaml | 38 + tests/main/user-data-handling/task.yaml | 27 + tests/main/whoami/successful_login.exp | 13 + tests/main/whoami/task.yaml | 18 + tests/main/writable-areas/task.yaml | 37 + tests/main/xauth-migration/task.yaml | 85 + tests/main/xdg-open-compat/task.yaml | 103 + tests/manual-tests.md | 236 + tests/nested/core-revert/task.yaml | 73 + tests/nested/extra-snaps-assertions/task.yaml | 67 + tests/nested/image-build/task.yaml | 25 + tests/nightly/docker/task.yaml | 37 + tests/nightly/unity/task.yaml | 31 + tests/regression/lp-1595444/task.yaml | 35 + tests/regression/lp-1597839/task.yaml | 13 + tests/regression/lp-1597842/task.yaml | 23 + tests/regression/lp-1599891/task.yaml | 12 + tests/regression/lp-1606277/task.yaml | 13 + tests/regression/lp-1607796/task.yaml | 12 + tests/regression/lp-1613845/task.yaml | 22 + tests/regression/lp-1615113/task.yaml | 13 + tests/regression/lp-1618683/task.yaml | 13 + tests/regression/lp-1630479/task.yaml | 27 + tests/regression/lp-1641885/task.yaml | 30 + tests/regression/lp-1644439/task.yaml | 48 + tests/regression/lp-1665004/task.yaml | 15 + tests/regression/lp-1667385/task.yaml | 17 + tests/regression/lp-1693042/task.yaml | 14 + tests/regression/lp-1704860/snap-env-query.sh | 1 + tests/regression/lp-1704860/task.yaml | 25 + tests/regression/lp-1732555/task.yaml | 15 + tests/unit/c-unit-tests-clang/task.yaml | 30 + tests/unit/c-unit-tests-gcc/task.yaml | 27 + tests/unit/gccgo/task.yaml | 17 + tests/unit/go/task.yaml | 30 + tests/upgrade/basic/task.yaml | 88 + tests/upgrade/snapd-xdg-open/task.yaml | 38 + tests/util/benchmark.sh | 21 + testutil/base.go | 51 + testutil/checkers.go | 152 + testutil/checkers_test.go | 258 + testutil/dbustest.go | 72 + testutil/exec.go | 139 + testutil/exec_test.go | 54 + timeout/timeout.go | 76 + timeout/timeout_test.go | 65 + timeutil/export_test.go | 28 + timeutil/schedule.go | 235 + timeutil/schedule_test.go | 242 + update-pot | 39 + userd/launcher.go | 88 + userd/launcher_test.go | 81 + userd/userd.go | 113 + vendor/vendor.json | 197 + wrappers/binaries.go | 91 + wrappers/binaries_test.go | 152 + wrappers/desktop.go | 250 + wrappers/desktop_test.go | 361 + wrappers/export_test.go | 43 + wrappers/services.go | 415 + wrappers/services_gen_test.go | 265 + wrappers/services_test.go | 472 + x11/xauth.go | 151 + x11/xauth_test.go | 74 + 1810 files changed, 318932 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 COPYING create mode 100644 HACKING.md create mode 100644 PULL_REQUEST_TEMPLATE.md create mode 100644 README.md create mode 100644 arch/arch.go create mode 100644 arch/arch_test.go create mode 100644 asserts/account.go create mode 100644 asserts/account_key.go create mode 100644 asserts/account_key_test.go create mode 100644 asserts/account_test.go create mode 100644 asserts/asserts.go create mode 100644 asserts/asserts_test.go create mode 100644 asserts/assertstest/assertstest.go create mode 100644 asserts/assertstest/assertstest_test.go create mode 100644 asserts/crypto.go create mode 100644 asserts/database.go create mode 100644 asserts/database_test.go create mode 100644 asserts/device_asserts.go create mode 100644 asserts/device_asserts_test.go create mode 100644 asserts/digest.go create mode 100644 asserts/digest_test.go create mode 100644 asserts/export_test.go create mode 100644 asserts/fetcher.go create mode 100644 asserts/fetcher_test.go create mode 100644 asserts/findwildcard.go create mode 100644 asserts/findwildcard_test.go create mode 100644 asserts/fsbackstore.go create mode 100644 asserts/fsbackstore_test.go create mode 100644 asserts/fsentryutils.go create mode 100644 asserts/fskeypairmgr.go create mode 100644 asserts/fskeypairmgr_test.go create mode 100644 asserts/gpgkeypairmgr.go create mode 100644 asserts/gpgkeypairmgr_test.go create mode 100644 asserts/header_checks.go create mode 100644 asserts/headers.go create mode 100644 asserts/headers_test.go create mode 100644 asserts/ifacedecls.go create mode 100644 asserts/ifacedecls_test.go create mode 100644 asserts/membackstore.go create mode 100644 asserts/membackstore_test.go create mode 100644 asserts/memkeypairmgr.go create mode 100644 asserts/memkeypairmgr_test.go create mode 100644 asserts/privkeys_for_test.go create mode 100644 asserts/repair.go create mode 100644 asserts/repair_test.go create mode 100644 asserts/signtool/sign.go create mode 100644 asserts/signtool/sign_test.go create mode 100644 asserts/snap_asserts.go create mode 100644 asserts/snap_asserts_test.go create mode 100644 asserts/snapasserts/snapasserts.go create mode 100644 asserts/snapasserts/snapasserts_test.go create mode 100644 asserts/store_asserts.go create mode 100644 asserts/store_asserts_test.go create mode 100644 asserts/sysdb/generic.go create mode 100644 asserts/sysdb/staging.go create mode 100644 asserts/sysdb/sysdb.go create mode 100644 asserts/sysdb/sysdb_test.go create mode 100644 asserts/sysdb/testkeys.go create mode 100644 asserts/sysdb/trusted.go create mode 100644 asserts/systestkeys/trusted.go create mode 100644 asserts/user.go create mode 100644 asserts/user_test.go create mode 100644 boot/boottest/mockbootloader.go create mode 100644 boot/kernel_os.go create mode 100644 boot/kernel_os_test.go create mode 100644 client/aliases.go create mode 100644 client/aliases_test.go create mode 100644 client/apps.go create mode 100644 client/apps_test.go create mode 100644 client/asserts.go create mode 100644 client/asserts_test.go create mode 100644 client/buy.go create mode 100644 client/change.go create mode 100644 client/change_test.go create mode 100644 client/client.go create mode 100644 client/client_test.go create mode 100644 client/conf.go create mode 100644 client/conf_test.go create mode 100644 client/export_test.go create mode 100644 client/icons.go create mode 100644 client/icons_test.go create mode 100644 client/interfaces.go create mode 100644 client/interfaces_test.go create mode 100644 client/login.go create mode 100644 client/login_test.go create mode 100644 client/packages.go create mode 100644 client/packages_test.go create mode 100644 client/snap_op.go create mode 100644 client/snap_op_test.go create mode 100644 client/snapctl.go create mode 100644 client/snapctl_test.go create mode 100644 cmd/.indent.pro create mode 100644 cmd/Makefile.am create mode 100755 cmd/autogen.sh create mode 100644 cmd/cmd.go create mode 100644 cmd/cmd_test.go create mode 100644 cmd/configure.ac create mode 100644 cmd/decode-mount-opts/decode-mount-opts.c create mode 100644 cmd/export_test.go create mode 100644 cmd/libsnap-confine-private/cgroup-freezer-support.c create mode 100644 cmd/libsnap-confine-private/cgroup-freezer-support.h create mode 100644 cmd/libsnap-confine-private/classic-test.c create mode 100644 cmd/libsnap-confine-private/classic.c create mode 100644 cmd/libsnap-confine-private/classic.h create mode 100644 cmd/libsnap-confine-private/cleanup-funcs-test.c create mode 100644 cmd/libsnap-confine-private/cleanup-funcs.c create mode 100644 cmd/libsnap-confine-private/cleanup-funcs.h create mode 100644 cmd/libsnap-confine-private/error-test.c create mode 100644 cmd/libsnap-confine-private/error.c create mode 100644 cmd/libsnap-confine-private/error.h create mode 100644 cmd/libsnap-confine-private/fault-injection-test.c create mode 100644 cmd/libsnap-confine-private/fault-injection.c create mode 100644 cmd/libsnap-confine-private/fault-injection.h create mode 100644 cmd/libsnap-confine-private/locking-test.c create mode 100644 cmd/libsnap-confine-private/locking.c create mode 100644 cmd/libsnap-confine-private/locking.h create mode 100644 cmd/libsnap-confine-private/mount-opt-test.c create mode 100644 cmd/libsnap-confine-private/mount-opt.c create mode 100644 cmd/libsnap-confine-private/mount-opt.h create mode 100644 cmd/libsnap-confine-private/mountinfo-test.c create mode 100644 cmd/libsnap-confine-private/mountinfo.c create mode 100644 cmd/libsnap-confine-private/mountinfo.h create mode 100644 cmd/libsnap-confine-private/privs-test.c create mode 100644 cmd/libsnap-confine-private/privs.c create mode 100644 cmd/libsnap-confine-private/privs.h create mode 100644 cmd/libsnap-confine-private/secure-getenv-test.c create mode 100644 cmd/libsnap-confine-private/secure-getenv.c create mode 100644 cmd/libsnap-confine-private/secure-getenv.h create mode 100644 cmd/libsnap-confine-private/snap-test.c create mode 100644 cmd/libsnap-confine-private/snap.c create mode 100644 cmd/libsnap-confine-private/snap.h create mode 100644 cmd/libsnap-confine-private/string-utils-test.c create mode 100644 cmd/libsnap-confine-private/string-utils.c create mode 100644 cmd/libsnap-confine-private/string-utils.h create mode 100644 cmd/libsnap-confine-private/test-utils-test.c create mode 100644 cmd/libsnap-confine-private/test-utils.c create mode 100644 cmd/libsnap-confine-private/test-utils.h create mode 100644 cmd/libsnap-confine-private/unit-tests-main.c create mode 100644 cmd/libsnap-confine-private/unit-tests.c create mode 100644 cmd/libsnap-confine-private/unit-tests.h create mode 100644 cmd/libsnap-confine-private/utils-test.c create mode 100644 cmd/libsnap-confine-private/utils.c create mode 100644 cmd/libsnap-confine-private/utils.h create mode 100644 cmd/snap-confine/PORTING create mode 100644 cmd/snap-confine/README.mount_namespace create mode 100644 cmd/snap-confine/README.nvidia create mode 100644 cmd/snap-confine/README.syscalls create mode 100644 cmd/snap-confine/apparmor-support.c create mode 100644 cmd/snap-confine/apparmor-support.h create mode 100644 cmd/snap-confine/cookie-support-test.c create mode 100644 cmd/snap-confine/cookie-support.c create mode 100644 cmd/snap-confine/cookie-support.h create mode 100644 cmd/snap-confine/misc/0001-Add-printk-based-debugging-to-pivot_root.patch create mode 100644 cmd/snap-confine/mount-support-nvidia.c create mode 100644 cmd/snap-confine/mount-support-nvidia.h create mode 100644 cmd/snap-confine/mount-support-test.c create mode 100644 cmd/snap-confine/mount-support.c create mode 100644 cmd/snap-confine/mount-support.h create mode 100644 cmd/snap-confine/ns-support-test.c create mode 100644 cmd/snap-confine/ns-support.c create mode 100644 cmd/snap-confine/ns-support.h create mode 100644 cmd/snap-confine/quirks.c create mode 100644 cmd/snap-confine/quirks.h create mode 100644 cmd/snap-confine/seccomp-support.c create mode 100644 cmd/snap-confine/seccomp-support.h create mode 100644 cmd/snap-confine/snap-confine-args-test.c create mode 100644 cmd/snap-confine/snap-confine-args.c create mode 100644 cmd/snap-confine/snap-confine-args.h create mode 100644 cmd/snap-confine/snap-confine.apparmor.in create mode 100644 cmd/snap-confine/snap-confine.c create mode 100644 cmd/snap-confine/snap-confine.rst create mode 100755 cmd/snap-confine/snappy-app-dev create mode 100644 cmd/snap-confine/spread-tests/data/apt-keys/README.md create mode 100644 cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.pub create mode 100644 cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.sec create mode 100644 cmd/snap-confine/spread-tests/distros/debian. create mode 100644 cmd/snap-confine/spread-tests/distros/debian.common create mode 100644 cmd/snap-confine/spread-tests/distros/ubuntu.14.04 create mode 100644 cmd/snap-confine/spread-tests/distros/ubuntu.16.04 create mode 100644 cmd/snap-confine/spread-tests/distros/ubuntu.16.10 create mode 100644 cmd/snap-confine/spread-tests/distros/ubuntu.common create mode 100644 cmd/snap-confine/spread-tests/main/cgroup-used/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/core-is-preferred/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/debug-flags/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/hostfs-created-on-demand/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/media-visible-in-devmode/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.amd64.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.i386.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.amd64.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.i386.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.amd64.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.i386.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.amd64.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.i386.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.json create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.linode.json create mode 100755 cmd/snap-confine/spread-tests/main/mount-ns-layout/process.py create mode 100755 cmd/snap-confine/spread-tests/main/mount-ns-layout/snap-arch.py create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-layout/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-ns-sharing/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-destination/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-source/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-missing-dst/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-missing-src/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-mount-tmpfs/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-ro-mount/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-profiles-rw-mount/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/mount-usr-src/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/test-seccomp-compat/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/test-snap-runs/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/ubuntu-core-launcher-exists/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/user-data-dir-created/task.yaml create mode 100644 cmd/snap-confine/spread-tests/main/user-xdg-runtime-dir-created/task.yaml create mode 100644 cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml create mode 100755 cmd/snap-confine/spread-tests/release.sh create mode 100755 cmd/snap-confine/spread-tests/spread-prepare.sh create mode 100644 cmd/snap-confine/udev-support.c create mode 100644 cmd/snap-confine/udev-support.h create mode 100644 cmd/snap-confine/user-support.c create mode 100644 cmd/snap-confine/user-support.h create mode 100644 cmd/snap-discard-ns/snap-discard-ns.c create mode 100644 cmd/snap-discard-ns/snap-discard-ns.rst create mode 100644 cmd/snap-exec/export_test.go create mode 100644 cmd/snap-exec/main.go create mode 100644 cmd/snap-exec/main_test.go create mode 100644 cmd/snap-repair/cmd_done_retry_skip.go create mode 100644 cmd/snap-repair/cmd_done_retry_skip_test.go create mode 100644 cmd/snap-repair/cmd_list.go create mode 100644 cmd/snap-repair/cmd_list_test.go create mode 100644 cmd/snap-repair/cmd_run.go create mode 100644 cmd/snap-repair/cmd_run_test.go create mode 100644 cmd/snap-repair/cmd_show.go create mode 100644 cmd/snap-repair/cmd_show_test.go create mode 100644 cmd/snap-repair/export_test.go create mode 100644 cmd/snap-repair/main.go create mode 100644 cmd/snap-repair/main_test.go create mode 100644 cmd/snap-repair/runner.go create mode 100644 cmd/snap-repair/runner_test.go create mode 100644 cmd/snap-repair/staging.go create mode 100644 cmd/snap-repair/trace.go create mode 100644 cmd/snap-repair/trace_test.go create mode 100644 cmd/snap-repair/trusted.go create mode 100644 cmd/snap-seccomp/export_test.go create mode 100644 cmd/snap-seccomp/main.go create mode 100644 cmd/snap-seccomp/main_ppc64le.go create mode 100644 cmd/snap-seccomp/main_test.go create mode 100644 cmd/snap-update-ns/bootstrap.c create mode 100644 cmd/snap-update-ns/bootstrap.go create mode 100644 cmd/snap-update-ns/bootstrap.h create mode 100644 cmd/snap-update-ns/bootstrap_ppc64le.go create mode 100644 cmd/snap-update-ns/bootstrap_test.go create mode 100644 cmd/snap-update-ns/change.go create mode 100644 cmd/snap-update-ns/change_test.go create mode 100644 cmd/snap-update-ns/entry.go create mode 100644 cmd/snap-update-ns/entry_test.go create mode 100644 cmd/snap-update-ns/export_test.go create mode 100644 cmd/snap-update-ns/freezer.go create mode 100644 cmd/snap-update-ns/freezer_test.go create mode 100644 cmd/snap-update-ns/main.go create mode 100644 cmd/snap-update-ns/main_test.go create mode 100644 cmd/snap-update-ns/sorting.go create mode 100644 cmd/snap-update-ns/sorting_test.go create mode 100644 cmd/snap-update-ns/utils.go create mode 100644 cmd/snap-update-ns/utils_test.go create mode 100644 cmd/snap/cmd_abort.go create mode 100644 cmd/snap/cmd_ack.go create mode 100644 cmd/snap/cmd_alias.go create mode 100644 cmd/snap/cmd_alias_test.go create mode 100644 cmd/snap/cmd_aliases.go create mode 100644 cmd/snap/cmd_aliases_test.go create mode 100644 cmd/snap/cmd_auto_import.go create mode 100644 cmd/snap/cmd_auto_import_test.go create mode 100644 cmd/snap/cmd_booted.go create mode 100644 cmd/snap/cmd_buy.go create mode 100644 cmd/snap/cmd_buy_test.go create mode 100644 cmd/snap/cmd_can_manage_refreshes.go create mode 100644 cmd/snap/cmd_changes.go create mode 100644 cmd/snap/cmd_changes_test.go create mode 100644 cmd/snap/cmd_confinement.go create mode 100644 cmd/snap/cmd_confinement_test.go create mode 100644 cmd/snap/cmd_connect.go create mode 100644 cmd/snap/cmd_connect_test.go create mode 100644 cmd/snap/cmd_create_key.go create mode 100644 cmd/snap/cmd_create_key_test.go create mode 100644 cmd/snap/cmd_create_user.go create mode 100644 cmd/snap/cmd_create_user_test.go create mode 100644 cmd/snap/cmd_debug.go create mode 100644 cmd/snap/cmd_delete_key.go create mode 100644 cmd/snap/cmd_delete_key_test.go create mode 100644 cmd/snap/cmd_disconnect.go create mode 100644 cmd/snap/cmd_disconnect_test.go create mode 100644 cmd/snap/cmd_download.go create mode 100644 cmd/snap/cmd_ensure_state_soon.go create mode 100644 cmd/snap/cmd_ensure_state_soon_test.go create mode 100644 cmd/snap/cmd_export_key.go create mode 100644 cmd/snap/cmd_export_key_test.go create mode 100644 cmd/snap/cmd_find.go create mode 100644 cmd/snap/cmd_find_test.go create mode 100644 cmd/snap/cmd_first_boot.go create mode 100644 cmd/snap/cmd_get.go create mode 100644 cmd/snap/cmd_get_base_declaration.go create mode 100644 cmd/snap/cmd_get_base_declaration_test.go create mode 100644 cmd/snap/cmd_get_test.go create mode 100644 cmd/snap/cmd_help.go create mode 100644 cmd/snap/cmd_help_test.go create mode 100644 cmd/snap/cmd_info.go create mode 100644 cmd/snap/cmd_info_test.go create mode 100644 cmd/snap/cmd_interface.go create mode 100644 cmd/snap/cmd_interface_test.go create mode 100644 cmd/snap/cmd_interfaces.go create mode 100644 cmd/snap/cmd_interfaces_test.go create mode 100644 cmd/snap/cmd_keys.go create mode 100644 cmd/snap/cmd_keys_test.go create mode 100644 cmd/snap/cmd_known.go create mode 100644 cmd/snap/cmd_known_test.go create mode 100644 cmd/snap/cmd_list.go create mode 100644 cmd/snap/cmd_list_test.go create mode 100644 cmd/snap/cmd_login.go create mode 100644 cmd/snap/cmd_login_test.go create mode 100644 cmd/snap/cmd_logout.go create mode 100644 cmd/snap/cmd_managed.go create mode 100644 cmd/snap/cmd_managed_test.go create mode 100644 cmd/snap/cmd_pack.go create mode 100644 cmd/snap/cmd_prefer.go create mode 100644 cmd/snap/cmd_prefer_test.go create mode 100644 cmd/snap/cmd_prepare_image.go create mode 100644 cmd/snap/cmd_repair_repairs.go create mode 100644 cmd/snap/cmd_repair_repairs_test.go create mode 100644 cmd/snap/cmd_run.go create mode 100644 cmd/snap/cmd_run_test.go create mode 100644 cmd/snap/cmd_services.go create mode 100644 cmd/snap/cmd_services_test.go create mode 100644 cmd/snap/cmd_set.go create mode 100644 cmd/snap/cmd_set_test.go create mode 100644 cmd/snap/cmd_shell.go create mode 100644 cmd/snap/cmd_sign.go create mode 100644 cmd/snap/cmd_sign_build.go create mode 100644 cmd/snap/cmd_sign_build_test.go create mode 100644 cmd/snap/cmd_sign_test.go create mode 100644 cmd/snap/cmd_snap_op.go create mode 100644 cmd/snap/cmd_snap_op_test.go create mode 100644 cmd/snap/cmd_unalias.go create mode 100644 cmd/snap/cmd_unalias_test.go create mode 100644 cmd/snap/cmd_userd.go create mode 100644 cmd/snap/cmd_userd_test.go create mode 100644 cmd/snap/cmd_version.go create mode 100644 cmd/snap/cmd_version_test.go create mode 100644 cmd/snap/cmd_watch.go create mode 100644 cmd/snap/cmd_watch_test.go create mode 100644 cmd/snap/cmd_whoami.go create mode 100644 cmd/snap/complete.go create mode 100644 cmd/snap/error.go create mode 100644 cmd/snap/export_test.go create mode 100644 cmd/snap/gnupg2_test.go create mode 100644 cmd/snap/interfaces_common.go create mode 100644 cmd/snap/interfaces_common_test.go create mode 100644 cmd/snap/last.go create mode 100644 cmd/snap/main.go create mode 100644 cmd/snap/main_test.go create mode 100644 cmd/snap/notes.go create mode 100644 cmd/snap/notes_test.go create mode 100644 cmd/snap/test-data/pubring.gpg create mode 100644 cmd/snap/test-data/secring.gpg create mode 100644 cmd/snap/test-data/trustdb.gpg create mode 100644 cmd/snapctl/main.go create mode 100644 cmd/snapctl/main_test.go create mode 100644 cmd/snapd/main.go create mode 100644 cmd/system-shutdown/system-shutdown-utils-test.c create mode 100644 cmd/system-shutdown/system-shutdown-utils.c create mode 100644 cmd/system-shutdown/system-shutdown-utils.h create mode 100644 cmd/system-shutdown/system-shutdown.c create mode 100644 cmd/version.go create mode 100644 corecfg/corecfg.go create mode 100644 corecfg/corecfg_test.go create mode 100644 corecfg/export_test.go create mode 100644 corecfg/picfg.go create mode 100644 corecfg/picfg_test.go create mode 100644 corecfg/powerbtn.go create mode 100644 corecfg/powerbtn_test.go create mode 100644 corecfg/proxy.go create mode 100644 corecfg/proxy_test.go create mode 100644 corecfg/refresh.go create mode 100644 corecfg/refresh_test.go create mode 100644 corecfg/services.go create mode 100644 corecfg/services_test.go create mode 100644 corecfg/utils.go create mode 100644 corecfg/utils_test.go create mode 100644 daemon/api.go create mode 100644 daemon/api_mock_test.go create mode 100644 daemon/api_test.go create mode 100644 daemon/daemon.go create mode 100644 daemon/daemon_test.go create mode 100644 daemon/response.go create mode 100644 daemon/response_test.go create mode 100644 daemon/snap.go create mode 100644 daemon/ucrednet.go create mode 100644 daemon/ucrednet_test.go create mode 100644 data/Makefile create mode 100644 data/completion/complete.sh create mode 100755 data/completion/etelpmoc.sh create mode 100644 data/completion/snap create mode 100644 data/dbus/Makefile create mode 100644 data/dbus/io.snapcraft.Launcher.service.in create mode 100644 data/env/Makefile create mode 100644 data/env/snapd.sh.in create mode 100644 data/failure.txt create mode 100644 data/polkit/io.snapcraft.snapd.policy create mode 100644 data/selinux/COPYING create mode 100644 data/selinux/INSTALL.md create mode 100644 data/selinux/Makefile create mode 100644 data/selinux/README.md create mode 100644 data/selinux/snappy.fc create mode 100644 data/selinux/snappy.if create mode 100644 data/selinux/snappy.te create mode 100644 data/success.txt create mode 100644 data/systemd/Makefile create mode 100644 data/systemd/snapd.autoimport.service.in create mode 100644 data/systemd/snapd.core-fixup.service.in create mode 100755 data/systemd/snapd.core-fixup.sh create mode 100644 data/systemd/snapd.refresh.service.in create mode 100644 data/systemd/snapd.refresh.timer create mode 100644 data/systemd/snapd.service.in create mode 100644 data/systemd/snapd.snap-repair.service.in create mode 100644 data/systemd/snapd.snap-repair.timer create mode 100644 data/systemd/snapd.socket create mode 100644 data/systemd/snapd.system-shutdown.service.in create mode 100644 data/udev/rules.d/66-snapd-autoimport.rules create mode 120000 debian create mode 100644 dirs/dirs.go create mode 100644 dirs/dirs_test.go create mode 100644 docs/MOVED.md create mode 100644 errtracker/errtracker.go create mode 100644 errtracker/errtracker_test.go create mode 100644 errtracker/export_test.go create mode 100755 gen-coverage.sh create mode 100755 generate-packaging-dir create mode 100755 get-deps.sh create mode 100644 httputil/export_test.go create mode 100644 httputil/logger.go create mode 100644 httputil/logger_test.go create mode 100644 httputil/redirect17.go create mode 100644 httputil/redirect18.go create mode 100644 httputil/retry.go create mode 100644 httputil/retry_test.go create mode 100644 httputil/transport16.go create mode 100644 httputil/transport17.go create mode 100644 httputil/useragent.go create mode 100644 httputil/useragent_test.go create mode 100644 httputil/withtestkeys.go create mode 100644 i18n/i18n.go create mode 100644 i18n/i18n_test.go create mode 100644 i18n/xgettext-go/main.go create mode 100644 i18n/xgettext-go/main_test.go create mode 100644 image/export_test.go create mode 100644 image/helpers.go create mode 100644 image/image.go create mode 100644 image/image_test.go create mode 100644 interfaces/apparmor/apparmor.go create mode 100644 interfaces/apparmor/apparmor_test.go create mode 100644 interfaces/apparmor/backend.go create mode 100644 interfaces/apparmor/backend_test.go create mode 100644 interfaces/apparmor/export_test.go create mode 100644 interfaces/apparmor/spec.go create mode 100644 interfaces/apparmor/spec_test.go create mode 100644 interfaces/apparmor/template.go create mode 100644 interfaces/apparmor/template_vars.go create mode 100644 interfaces/backend.go create mode 100644 interfaces/backends/backends.go create mode 100644 interfaces/backends/backends_test.go create mode 100644 interfaces/backends/export_test.go create mode 100644 interfaces/builtin/account_control.go create mode 100644 interfaces/builtin/account_control_test.go create mode 100644 interfaces/builtin/all.go create mode 100644 interfaces/builtin/all_test.go create mode 100644 interfaces/builtin/alsa.go create mode 100644 interfaces/builtin/alsa_test.go create mode 100644 interfaces/builtin/autopilot.go create mode 100644 interfaces/builtin/autopilot_test.go create mode 100644 interfaces/builtin/avahi_control.go create mode 100644 interfaces/builtin/avahi_control_test.go create mode 100644 interfaces/builtin/avahi_observe.go create mode 100644 interfaces/builtin/avahi_observe_test.go create mode 100644 interfaces/builtin/bluetooth_control.go create mode 100644 interfaces/builtin/bluetooth_control_test.go create mode 100644 interfaces/builtin/bluez.go create mode 100644 interfaces/builtin/bluez_test.go create mode 100644 interfaces/builtin/bool_file.go create mode 100644 interfaces/builtin/bool_file_test.go create mode 100644 interfaces/builtin/broadcom_asic_control.go create mode 100644 interfaces/builtin/broadcom_asic_control_test.go create mode 100644 interfaces/builtin/browser_support.go create mode 100644 interfaces/builtin/browser_support_test.go create mode 100644 interfaces/builtin/camera.go create mode 100644 interfaces/builtin/camera_test.go create mode 100644 interfaces/builtin/classic_support.go create mode 100644 interfaces/builtin/classic_support_test.go create mode 100644 interfaces/builtin/common.go create mode 100644 interfaces/builtin/common_test.go create mode 100644 interfaces/builtin/content.go create mode 100644 interfaces/builtin/content_test.go create mode 100644 interfaces/builtin/core_support.go create mode 100644 interfaces/builtin/core_support_test.go create mode 100644 interfaces/builtin/cups_control.go create mode 100644 interfaces/builtin/dbus.go create mode 100644 interfaces/builtin/dbus_test.go create mode 100644 interfaces/builtin/dcdbas_control.go create mode 100644 interfaces/builtin/dcdbas_control_test.go create mode 100644 interfaces/builtin/desktop.go create mode 100644 interfaces/builtin/desktop_legacy.go create mode 100644 interfaces/builtin/desktop_legacy_test.go create mode 100644 interfaces/builtin/desktop_test.go create mode 100644 interfaces/builtin/docker.go create mode 100644 interfaces/builtin/docker_support.go create mode 100644 interfaces/builtin/docker_support_test.go create mode 100644 interfaces/builtin/docker_test.go create mode 100644 interfaces/builtin/export_test.go create mode 100644 interfaces/builtin/firewall_control.go create mode 100644 interfaces/builtin/firewall_control_test.go create mode 100644 interfaces/builtin/framebuffer.go create mode 100644 interfaces/builtin/framebuffer_test.go create mode 100644 interfaces/builtin/fuse_support.go create mode 100644 interfaces/builtin/fuse_support_test.go create mode 100644 interfaces/builtin/fwupd.go create mode 100644 interfaces/builtin/fwupd_test.go create mode 100644 interfaces/builtin/gpg_keys.go create mode 100644 interfaces/builtin/gpg_keys_test.go create mode 100644 interfaces/builtin/gpg_public_keys.go create mode 100644 interfaces/builtin/gpg_public_keys_test.go create mode 100644 interfaces/builtin/gpio.go create mode 100644 interfaces/builtin/gpio_test.go create mode 100644 interfaces/builtin/greengrass_support.go create mode 100644 interfaces/builtin/greengrass_support_test.go create mode 100644 interfaces/builtin/gsettings.go create mode 100644 interfaces/builtin/gsettings_test.go create mode 100644 interfaces/builtin/hardware_observe.go create mode 100644 interfaces/builtin/hardware_observe_test.go create mode 100644 interfaces/builtin/hardware_random_control.go create mode 100644 interfaces/builtin/hardware_random_control_test.go create mode 100644 interfaces/builtin/hardware_random_observe.go create mode 100644 interfaces/builtin/hardware_random_observe_test.go create mode 100644 interfaces/builtin/hidraw.go create mode 100644 interfaces/builtin/hidraw_test.go create mode 100644 interfaces/builtin/home.go create mode 100644 interfaces/builtin/home_test.go create mode 100644 interfaces/builtin/i2c.go create mode 100644 interfaces/builtin/i2c_test.go create mode 100644 interfaces/builtin/iio.go create mode 100644 interfaces/builtin/iio_test.go create mode 100644 interfaces/builtin/io_ports_control.go create mode 100644 interfaces/builtin/io_ports_control_test.go create mode 100644 interfaces/builtin/joystick.go create mode 100644 interfaces/builtin/joystick_test.go create mode 100644 interfaces/builtin/kernel_module_control.go create mode 100644 interfaces/builtin/kernel_module_control_test.go create mode 100644 interfaces/builtin/kubernetes_support.go create mode 100644 interfaces/builtin/kubernetes_support_test.go create mode 100644 interfaces/builtin/kvm.go create mode 100644 interfaces/builtin/kvm_test.go create mode 100644 interfaces/builtin/libvirt.go create mode 100644 interfaces/builtin/libvirt_test.go create mode 100644 interfaces/builtin/locale_control.go create mode 100644 interfaces/builtin/locale_control_test.go create mode 100644 interfaces/builtin/location_control.go create mode 100644 interfaces/builtin/location_control_test.go create mode 100644 interfaces/builtin/location_observe.go create mode 100644 interfaces/builtin/location_observe_test.go create mode 100644 interfaces/builtin/log_observe.go create mode 100644 interfaces/builtin/log_observe_test.go create mode 100644 interfaces/builtin/lxd.go create mode 100644 interfaces/builtin/lxd_support.go create mode 100644 interfaces/builtin/lxd_support_test.go create mode 100644 interfaces/builtin/lxd_test.go create mode 100644 interfaces/builtin/maliit.go create mode 100644 interfaces/builtin/maliit_test.go create mode 100644 interfaces/builtin/media_hub.go create mode 100644 interfaces/builtin/media_hub_test.go create mode 100644 interfaces/builtin/mir.go create mode 100644 interfaces/builtin/mir_test.go create mode 100644 interfaces/builtin/modem_manager.go create mode 100644 interfaces/builtin/modem_manager_test.go create mode 100644 interfaces/builtin/mount_observe.go create mode 100644 interfaces/builtin/mount_observe_test.go create mode 100644 interfaces/builtin/mpris.go create mode 100644 interfaces/builtin/mpris_test.go create mode 100644 interfaces/builtin/netlink_audit.go create mode 100644 interfaces/builtin/netlink_audit_test.go create mode 100644 interfaces/builtin/netlink_connector.go create mode 100644 interfaces/builtin/netlink_connector_test.go create mode 100644 interfaces/builtin/network.go create mode 100644 interfaces/builtin/network_bind.go create mode 100644 interfaces/builtin/network_bind_test.go create mode 100644 interfaces/builtin/network_control.go create mode 100644 interfaces/builtin/network_control_test.go create mode 100644 interfaces/builtin/network_manager.go create mode 100644 interfaces/builtin/network_manager_test.go create mode 100644 interfaces/builtin/network_observe.go create mode 100644 interfaces/builtin/network_observe_test.go create mode 100644 interfaces/builtin/network_setup_control.go create mode 100644 interfaces/builtin/network_setup_control_test.go create mode 100644 interfaces/builtin/network_setup_observe.go create mode 100644 interfaces/builtin/network_setup_observe_test.go create mode 100644 interfaces/builtin/network_status.go create mode 100644 interfaces/builtin/network_status_test.go create mode 100644 interfaces/builtin/network_test.go create mode 100644 interfaces/builtin/ofono.go create mode 100644 interfaces/builtin/ofono_test.go create mode 100644 interfaces/builtin/online_accounts_service.go create mode 100644 interfaces/builtin/online_accounts_service_test.go create mode 100644 interfaces/builtin/opengl.go create mode 100644 interfaces/builtin/opengl_test.go create mode 100644 interfaces/builtin/openvswitch.go create mode 100644 interfaces/builtin/openvswitch_support.go create mode 100644 interfaces/builtin/openvswitch_support_test.go create mode 100644 interfaces/builtin/openvswitch_test.go create mode 100644 interfaces/builtin/optical_drive.go create mode 100644 interfaces/builtin/optical_drive_test.go create mode 100644 interfaces/builtin/password_manager_service.go create mode 100644 interfaces/builtin/password_manager_service_test.go create mode 100644 interfaces/builtin/physical_memory_control.go create mode 100644 interfaces/builtin/physical_memory_control_test.go create mode 100644 interfaces/builtin/physical_memory_observe.go create mode 100644 interfaces/builtin/physical_memory_observe_test.go create mode 100644 interfaces/builtin/ppp.go create mode 100644 interfaces/builtin/ppp_test.go create mode 100644 interfaces/builtin/process_control.go create mode 100644 interfaces/builtin/process_control_test.go create mode 100644 interfaces/builtin/pulseaudio.go create mode 100644 interfaces/builtin/pulseaudio_test.go create mode 100644 interfaces/builtin/raw_usb.go create mode 100644 interfaces/builtin/raw_usb_test.go create mode 100644 interfaces/builtin/removable_media.go create mode 100644 interfaces/builtin/removable_media_test.go create mode 100644 interfaces/builtin/screen_inhibit_control.go create mode 100644 interfaces/builtin/screen_inhibit_control_test.go create mode 100644 interfaces/builtin/serial_port.go create mode 100644 interfaces/builtin/serial_port_test.go create mode 100644 interfaces/builtin/shutdown.go create mode 100644 interfaces/builtin/shutdown_test.go create mode 100644 interfaces/builtin/snapd_control.go create mode 100644 interfaces/builtin/snapd_control_test.go create mode 100644 interfaces/builtin/spi.go create mode 100644 interfaces/builtin/spi_test.go create mode 100644 interfaces/builtin/ssh_keys.go create mode 100644 interfaces/builtin/ssh_keys_test.go create mode 100644 interfaces/builtin/ssh_public_keys.go create mode 100644 interfaces/builtin/ssh_public_keys_test.go create mode 100644 interfaces/builtin/storage_framework_service.go create mode 100644 interfaces/builtin/storage_framework_service_test.go create mode 100644 interfaces/builtin/system_observe.go create mode 100644 interfaces/builtin/system_observe_test.go create mode 100644 interfaces/builtin/system_trace.go create mode 100644 interfaces/builtin/system_trace_test.go create mode 100644 interfaces/builtin/thumbnailer_service.go create mode 100644 interfaces/builtin/thumbnailer_service_test.go create mode 100644 interfaces/builtin/time_control.go create mode 100644 interfaces/builtin/time_control_test.go create mode 100644 interfaces/builtin/timeserver_control.go create mode 100644 interfaces/builtin/timeserver_control_test.go create mode 100644 interfaces/builtin/timezone_control.go create mode 100644 interfaces/builtin/timezone_control_test.go create mode 100644 interfaces/builtin/tpm.go create mode 100644 interfaces/builtin/tpm_test.go create mode 100644 interfaces/builtin/ubuntu_download_manager.go create mode 100644 interfaces/builtin/ubuntu_download_manager_test.go create mode 100644 interfaces/builtin/udisks2.go create mode 100644 interfaces/builtin/udisks2_test.go create mode 100644 interfaces/builtin/uhid.go create mode 100644 interfaces/builtin/uhid_test.go create mode 100644 interfaces/builtin/unity7.go create mode 100644 interfaces/builtin/unity7_test.go create mode 100644 interfaces/builtin/unity8.go create mode 100644 interfaces/builtin/unity8_calendar.go create mode 100644 interfaces/builtin/unity8_calendar_test.go create mode 100644 interfaces/builtin/unity8_contacts.go create mode 100644 interfaces/builtin/unity8_contacts_test.go create mode 100644 interfaces/builtin/unity8_pim_common.go create mode 100644 interfaces/builtin/unity8_test.go create mode 100644 interfaces/builtin/upower_observe.go create mode 100644 interfaces/builtin/upower_observe_test.go create mode 100644 interfaces/builtin/utils.go create mode 100644 interfaces/builtin/utils_test.go create mode 100644 interfaces/builtin/wayland.go create mode 100644 interfaces/builtin/wayland_test.go create mode 100644 interfaces/builtin/x11.go create mode 100644 interfaces/builtin/x11_test.go create mode 100644 interfaces/connection.go create mode 100644 interfaces/connection_test.go create mode 100644 interfaces/core.go create mode 100644 interfaces/core_test.go create mode 100644 interfaces/dbus/backend.go create mode 100644 interfaces/dbus/backend_test.go create mode 100644 interfaces/dbus/dbus.go create mode 100644 interfaces/dbus/dbus_test.go create mode 100644 interfaces/dbus/export_test.go create mode 100644 interfaces/dbus/spec.go create mode 100644 interfaces/dbus/spec_test.go create mode 100644 interfaces/dbus/template.go create mode 100644 interfaces/export_test.go create mode 100644 interfaces/ifacetest/backend.go create mode 100644 interfaces/ifacetest/backendtest.go create mode 100644 interfaces/ifacetest/ifacetest_test.go create mode 100644 interfaces/ifacetest/spec.go create mode 100644 interfaces/ifacetest/spec_test.go create mode 100644 interfaces/ifacetest/testiface.go create mode 100644 interfaces/ifacetest/testiface_test.go create mode 100644 interfaces/json.go create mode 100644 interfaces/json_test.go create mode 100644 interfaces/kmod/backend.go create mode 100644 interfaces/kmod/backend_test.go create mode 100644 interfaces/kmod/export_test.go create mode 100644 interfaces/kmod/kmod.go create mode 100644 interfaces/kmod/kmod_test.go create mode 100644 interfaces/kmod/spec.go create mode 100644 interfaces/kmod/spec_test.go create mode 100644 interfaces/mount/backend.go create mode 100644 interfaces/mount/backend_test.go create mode 100644 interfaces/mount/entry.go create mode 100644 interfaces/mount/entry_test.go create mode 100644 interfaces/mount/lock.go create mode 100644 interfaces/mount/lock_test.go create mode 100644 interfaces/mount/mountinfo.go create mode 100644 interfaces/mount/mountinfo_test.go create mode 100644 interfaces/mount/ns.go create mode 100644 interfaces/mount/ns_test.go create mode 100644 interfaces/mount/profile.go create mode 100644 interfaces/mount/profile_test.go create mode 100644 interfaces/mount/spec.go create mode 100644 interfaces/mount/spec_test.go create mode 100644 interfaces/naming.go create mode 100644 interfaces/naming_test.go create mode 100644 interfaces/policy/basedeclaration.go create mode 100644 interfaces/policy/basedeclaration_test.go create mode 100644 interfaces/policy/export_test.go create mode 100644 interfaces/policy/helpers.go create mode 100644 interfaces/policy/helpers_test.go create mode 100644 interfaces/policy/policy.go create mode 100644 interfaces/policy/policy_test.go create mode 100644 interfaces/repo.go create mode 100644 interfaces/repo_test.go create mode 100644 interfaces/seccomp/backend.go create mode 100644 interfaces/seccomp/backend_test.go create mode 100644 interfaces/seccomp/export_test.go create mode 100644 interfaces/seccomp/seccomp_test.go create mode 100644 interfaces/seccomp/spec.go create mode 100644 interfaces/seccomp/spec_test.go create mode 100644 interfaces/seccomp/template.go create mode 100644 interfaces/sorting.go create mode 100644 interfaces/sorting_test.go create mode 100644 interfaces/systemd/backend.go create mode 100644 interfaces/systemd/backend_test.go create mode 100644 interfaces/systemd/service.go create mode 100644 interfaces/systemd/service_test.go create mode 100644 interfaces/systemd/spec.go create mode 100644 interfaces/systemd/spec_test.go create mode 100644 interfaces/systemd/systemd_test.go create mode 100644 interfaces/udev/backend.go create mode 100644 interfaces/udev/backend_test.go create mode 100644 interfaces/udev/spec.go create mode 100644 interfaces/udev/spec_test.go create mode 100644 interfaces/udev/udev.go create mode 100644 interfaces/udev/udev_test.go create mode 100644 jsonutil/json.go create mode 100644 jsonutil/json_test.go create mode 100644 logger/export_test.go create mode 100644 logger/logger.go create mode 100644 logger/logger_test.go create mode 100755 mdlint.py create mode 100755 mkversion.sh create mode 100644 osutil/bootid.go create mode 100644 osutil/bootid_test.go create mode 100644 osutil/buildid.go create mode 100644 osutil/buildid_test.go create mode 100644 osutil/chattr.go create mode 100644 osutil/chattr_32.go create mode 100644 osutil/chattr_64.go create mode 100644 osutil/chdir.go create mode 100644 osutil/chdir_test.go create mode 100644 osutil/cmp.go create mode 100644 osutil/cmp_test.go create mode 100644 osutil/cp.go create mode 100644 osutil/cp_linux.go create mode 100644 osutil/cp_linux_test.go create mode 100644 osutil/cp_other.go create mode 100644 osutil/cp_test.go create mode 100644 osutil/digest.go create mode 100644 osutil/digest_test.go create mode 100644 osutil/env.go create mode 100644 osutil/env_test.go create mode 100644 osutil/exec.go create mode 100644 osutil/exec_test.go create mode 100644 osutil/exitcode.go create mode 100644 osutil/exitcode_test.go create mode 100644 osutil/export_test.go create mode 100644 osutil/flock.go create mode 100644 osutil/flock_test.go create mode 100644 osutil/fshelpers.go create mode 100644 osutil/fshelpers_test.go create mode 100644 osutil/group.go create mode 100644 osutil/io.go create mode 100644 osutil/io_test.go create mode 100644 osutil/mkdirallchown.go create mode 100644 osutil/mount.go create mode 100644 osutil/mount_test.go create mode 100644 osutil/osutil_test.go create mode 100644 osutil/outputerr.go create mode 100644 osutil/outputerr_test.go create mode 100644 osutil/stat.go create mode 100644 osutil/stat_test.go create mode 100644 osutil/syncdir.go create mode 100644 osutil/syncdir_test.go create mode 100644 osutil/user.go create mode 100644 osutil/user_test.go create mode 100644 osutil/winsize.go create mode 100644 overlord/assertstate/assertmgr.go create mode 100644 overlord/assertstate/assertstate.go create mode 100644 overlord/assertstate/assertstate_test.go create mode 100644 overlord/assertstate/export_test.go create mode 100644 overlord/assertstate/helpers.go create mode 100644 overlord/auth/auth.go create mode 100644 overlord/auth/auth_test.go create mode 100644 overlord/backend.go create mode 100644 overlord/cmdstate/cmdmgr.go create mode 100644 overlord/cmdstate/cmdstate.go create mode 100644 overlord/cmdstate/cmdstate_test.go create mode 100644 overlord/cmdstate/export_test.go create mode 100644 overlord/configstate/config/helpers.go create mode 100644 overlord/configstate/config/helpers_test.go create mode 100644 overlord/configstate/config/transaction.go create mode 100644 overlord/configstate/config/transaction_test.go create mode 100644 overlord/configstate/configmgr.go create mode 100644 overlord/configstate/configstate.go create mode 100644 overlord/configstate/configstate_test.go create mode 100644 overlord/configstate/export_test.go create mode 100644 overlord/configstate/handler_test.go create mode 100644 overlord/configstate/hooks.go create mode 100644 overlord/devicestate/crypto.go create mode 100644 overlord/devicestate/devicemgr.go create mode 100644 overlord/devicestate/devicestate.go create mode 100644 overlord/devicestate/devicestate_test.go create mode 100644 overlord/devicestate/export_test.go create mode 100644 overlord/devicestate/firstboot.go create mode 100644 overlord/devicestate/firstboot_test.go create mode 100644 overlord/devicestate/handlers.go create mode 100644 overlord/export_test.go create mode 100644 overlord/hookstate/context.go create mode 100644 overlord/hookstate/context_test.go create mode 100644 overlord/hookstate/ctlcmd/ctlcmd.go create mode 100644 overlord/hookstate/ctlcmd/ctlcmd_test.go create mode 100644 overlord/hookstate/ctlcmd/export_test.go create mode 100644 overlord/hookstate/ctlcmd/get.go create mode 100644 overlord/hookstate/ctlcmd/get_test.go create mode 100644 overlord/hookstate/ctlcmd/helpers.go create mode 100644 overlord/hookstate/ctlcmd/restart.go create mode 100644 overlord/hookstate/ctlcmd/services_test.go create mode 100644 overlord/hookstate/ctlcmd/set.go create mode 100644 overlord/hookstate/ctlcmd/set_test.go create mode 100644 overlord/hookstate/ctlcmd/start.go create mode 100644 overlord/hookstate/ctlcmd/stop.go create mode 100644 overlord/hookstate/export_test.go create mode 100644 overlord/hookstate/hookmgr.go create mode 100644 overlord/hookstate/hooks.go create mode 100644 overlord/hookstate/hookstate.go create mode 100644 overlord/hookstate/hookstate_test.go create mode 100644 overlord/hookstate/hooktest/handler.go create mode 100644 overlord/hookstate/hooktest/handler_test.go create mode 100644 overlord/hookstate/repository.go create mode 100644 overlord/hookstate/repository_test.go create mode 100644 overlord/ifacestate/export_test.go create mode 100644 overlord/ifacestate/handlers.go create mode 100644 overlord/ifacestate/helpers.go create mode 100644 overlord/ifacestate/hooks.go create mode 100644 overlord/ifacestate/ifacemgr.go create mode 100644 overlord/ifacestate/ifacerepo/repo.go create mode 100644 overlord/ifacestate/ifacerepo/repo_test.go create mode 100644 overlord/ifacestate/ifacestate.go create mode 100644 overlord/ifacestate/ifacestate_test.go create mode 100644 overlord/ifacestate/implicit.go create mode 100644 overlord/ifacestate/implicit_test.go create mode 100644 overlord/managers_test.go create mode 100644 overlord/overlord.go create mode 100644 overlord/overlord_test.go create mode 100644 overlord/patch/export_test.go create mode 100644 overlord/patch/patch.go create mode 100644 overlord/patch/patch1.go create mode 100644 overlord/patch/patch1_test.go create mode 100644 overlord/patch/patch2.go create mode 100644 overlord/patch/patch2_test.go create mode 100644 overlord/patch/patch3.go create mode 100644 overlord/patch/patch3_test.go create mode 100644 overlord/patch/patch4.go create mode 100644 overlord/patch/patch4_test.go create mode 100644 overlord/patch/patch5.go create mode 100644 overlord/patch/patch6.go create mode 100644 overlord/patch/patch6_test.go create mode 100644 overlord/patch/patch_test.go create mode 100644 overlord/servicestate/servicestate.go create mode 100644 overlord/snapstate/aliasesv2.go create mode 100644 overlord/snapstate/aliasesv2_test.go create mode 100644 overlord/snapstate/autorefresh.go create mode 100644 overlord/snapstate/autorefresh_test.go create mode 100644 overlord/snapstate/backend.go create mode 100644 overlord/snapstate/backend/aliases.go create mode 100644 overlord/snapstate/backend/aliases_test.go create mode 100644 overlord/snapstate/backend/backend.go create mode 100644 overlord/snapstate/backend/backend_test.go create mode 100644 overlord/snapstate/backend/copydata.go create mode 100644 overlord/snapstate/backend/copydata_test.go create mode 100644 overlord/snapstate/backend/export_test.go create mode 100644 overlord/snapstate/backend/link.go create mode 100644 overlord/snapstate/backend/link_test.go create mode 100644 overlord/snapstate/backend/mountns.go create mode 100644 overlord/snapstate/backend/mountunit.go create mode 100644 overlord/snapstate/backend/mountunit_test.go create mode 100644 overlord/snapstate/backend/setup.go create mode 100644 overlord/snapstate/backend/setup_test.go create mode 100644 overlord/snapstate/backend/snapdata.go create mode 100644 overlord/snapstate/backend/utils.go create mode 100644 overlord/snapstate/backend_test.go create mode 100644 overlord/snapstate/booted.go create mode 100644 overlord/snapstate/booted_test.go create mode 100644 overlord/snapstate/catalogrefresh.go create mode 100644 overlord/snapstate/catalogrefresh_test.go create mode 100644 overlord/snapstate/check_snap.go create mode 100644 overlord/snapstate/check_snap_test.go create mode 100644 overlord/snapstate/cookies.go create mode 100644 overlord/snapstate/cookies_test.go create mode 100644 overlord/snapstate/export_test.go create mode 100644 overlord/snapstate/flags.go create mode 100644 overlord/snapstate/handlers.go create mode 100644 overlord/snapstate/handlers_aliasesv2_test.go create mode 100644 overlord/snapstate/handlers_discard_test.go create mode 100644 overlord/snapstate/handlers_download_test.go create mode 100644 overlord/snapstate/handlers_link_test.go create mode 100644 overlord/snapstate/handlers_mount_test.go create mode 100644 overlord/snapstate/handlers_prepare_test.go create mode 100644 overlord/snapstate/progress.go create mode 100644 overlord/snapstate/progress_test.go create mode 100644 overlord/snapstate/readme.go create mode 100644 overlord/snapstate/readme_test.go create mode 100644 overlord/snapstate/refreshhints.go create mode 100644 overlord/snapstate/refreshhints_test.go create mode 100644 overlord/snapstate/snapmgr.go create mode 100644 overlord/snapstate/snapstate.go create mode 100644 overlord/snapstate/snapstate_test.go create mode 100644 overlord/snapstate/storehelpers.go create mode 100644 overlord/state/change.go create mode 100644 overlord/state/change_test.go create mode 100644 overlord/state/export_test.go create mode 100644 overlord/state/state.go create mode 100644 overlord/state/state_test.go create mode 100644 overlord/state/task.go create mode 100644 overlord/state/task_test.go create mode 100644 overlord/state/taskrunner.go create mode 100644 overlord/state/taskrunner_test.go create mode 100644 overlord/stateengine.go create mode 100644 overlord/stateengine_test.go create mode 100644 packaging/arch/PKGBUILD create mode 100644 packaging/arch/snapd.install create mode 120000 packaging/fedora-25 create mode 120000 packaging/fedora-26 create mode 120000 packaging/fedora-27 create mode 120000 packaging/fedora-rawhide create mode 100644 packaging/fedora/snap-mgmt.sh create mode 100644 packaging/fedora/snapd.spec create mode 120000 packaging/opensuse-42.1 create mode 100644 packaging/opensuse-42.2/permissions create mode 100644 packaging/opensuse-42.2/permissions.easy create mode 100644 packaging/opensuse-42.2/permissions.paranoid create mode 100644 packaging/opensuse-42.2/permissions.secure create mode 100644 packaging/opensuse-42.2/snapd-rpmlintrc create mode 100644 packaging/opensuse-42.2/snapd.changes create mode 100644 packaging/opensuse-42.2/snapd.spec create mode 100644 packaging/ubuntu-14.04/changelog create mode 120000 packaging/ubuntu-14.04/compat create mode 100644 packaging/ubuntu-14.04/control create mode 100644 packaging/ubuntu-14.04/copyright create mode 120000 packaging/ubuntu-14.04/golang-github-snapcore-snapd-dev.install create mode 100755 packaging/ubuntu-14.04/rules create mode 120000 packaging/ubuntu-14.04/snap-confine.maintscript create mode 100644 packaging/ubuntu-14.04/snap.mount.service create mode 120000 packaging/ubuntu-14.04/snapd.autoimport.udev create mode 100644 packaging/ubuntu-14.04/snapd.dirs create mode 100644 packaging/ubuntu-14.04/snapd.install create mode 120000 packaging/ubuntu-14.04/snapd.maintscript create mode 120000 packaging/ubuntu-14.04/snapd.manpages create mode 100644 packaging/ubuntu-14.04/snapd.postinst create mode 100644 packaging/ubuntu-14.04/snapd.postrm create mode 100644 packaging/ubuntu-14.04/snapd.prerm create mode 120000 packaging/ubuntu-14.04/source create mode 120000 packaging/ubuntu-14.04/tests/README.md create mode 100644 packaging/ubuntu-14.04/tests/control create mode 100644 packaging/ubuntu-14.04/tests/integrationtests create mode 120000 packaging/ubuntu-14.04/tests/testconfig.json create mode 100644 packaging/ubuntu-14.04/ubuntu-snappy-cli.dirs create mode 100644 packaging/ubuntu-16.04/changelog create mode 100644 packaging/ubuntu-16.04/compat create mode 100644 packaging/ubuntu-16.04/control create mode 100644 packaging/ubuntu-16.04/copyright create mode 100644 packaging/ubuntu-16.04/golang-github-snapcore-snapd-dev.install create mode 100755 packaging/ubuntu-16.04/rules create mode 100644 packaging/ubuntu-16.04/snap-confine.maintscript create mode 100644 packaging/ubuntu-16.04/snapd.autoimport.udev create mode 100644 packaging/ubuntu-16.04/snapd.dirs create mode 100644 packaging/ubuntu-16.04/snapd.install create mode 100644 packaging/ubuntu-16.04/snapd.maintscript create mode 100644 packaging/ubuntu-16.04/snapd.manpages create mode 100644 packaging/ubuntu-16.04/snapd.postinst create mode 100644 packaging/ubuntu-16.04/snapd.postrm create mode 100644 packaging/ubuntu-16.04/source/format create mode 100644 packaging/ubuntu-16.04/tests/README.md create mode 100644 packaging/ubuntu-16.04/tests/control create mode 100644 packaging/ubuntu-16.04/tests/integrationtests create mode 100644 packaging/ubuntu-16.04/tests/testconfig.json create mode 100644 packaging/ubuntu-16.04/ubuntu-snappy-cli.dirs create mode 120000 packaging/ubuntu-16.10 create mode 120000 packaging/ubuntu-17.04 create mode 100644 partition/androidboot.go create mode 100644 partition/androidboot_test.go create mode 100644 partition/androidbootenv/androidbootenv.go create mode 100644 partition/androidbootenv/androidbootenv_test.go create mode 100644 partition/bootloader.go create mode 100644 partition/bootloader_test.go create mode 100644 partition/export_test.go create mode 100644 partition/grub.go create mode 100644 partition/grub_test.go create mode 100644 partition/grubenv/grubenv.go create mode 100644 partition/grubenv/grubenv_test.go create mode 100644 partition/uboot.go create mode 100644 partition/uboot_test.go create mode 100644 partition/ubootenv/env.go create mode 100644 partition/ubootenv/env_test.go create mode 100644 partition/ubootenv/export_test.go create mode 100644 partition/utils.go create mode 100644 partition/utils_test.go create mode 100644 po/am.po create mode 100644 po/bs.po create mode 100644 po/ca.po create mode 100644 po/cs.po create mode 100644 po/da.po create mode 100644 po/de.po create mode 100644 po/el.po create mode 100644 po/en_GB.po create mode 100644 po/es.po create mode 100644 po/fi.po create mode 100644 po/fr.po create mode 100644 po/gl.po create mode 100644 po/hr.po create mode 100644 po/ia.po create mode 100644 po/id.po create mode 100644 po/it.po create mode 100644 po/ja.po create mode 100644 po/lt.po create mode 100644 po/ms.po create mode 100644 po/nb.po create mode 100644 po/oc.po create mode 100644 po/pt.po create mode 100644 po/pt_BR.po create mode 100644 po/ru.po create mode 100644 po/sv.po create mode 100644 po/tr.po create mode 100644 po/ug.po create mode 100644 po/zh_CN.po create mode 100644 polkit/authority.go create mode 100644 polkit/pid_start_time.go create mode 100644 polkit/pid_start_time_test.go create mode 100644 progress/ansimeter.go create mode 100644 progress/ansimeter_test.go create mode 100644 progress/example_test.go create mode 100644 progress/export_test.go create mode 100644 progress/progress.go create mode 100644 progress/progress_test.go create mode 100644 progress/progresstest/progresstest.go create mode 100644 progress/quantity.go create mode 100644 release/apparmor.go create mode 100644 release/apparmor_test.go create mode 100644 release/export_linux_test.go create mode 100644 release/export_test.go create mode 100644 release/release.go create mode 100644 release/release_test.go create mode 100644 release/uname_linux.go create mode 100644 release/uname_linux_test.go create mode 100755 run-checks create mode 100644 snap/broken.go create mode 100644 snap/broken_test.go create mode 100644 snap/container.go create mode 100644 snap/container_test.go create mode 100644 snap/epoch.go create mode 100644 snap/epoch_test.go create mode 100644 snap/errors.go create mode 100644 snap/export_test.go create mode 100644 snap/gadget.go create mode 100644 snap/gadget_test.go create mode 100644 snap/hooktypes.go create mode 100644 snap/implicit.go create mode 100644 snap/implicit_test.go create mode 100644 snap/info.go create mode 100644 snap/info_snap_yaml.go create mode 100644 snap/info_snap_yaml_test.go create mode 100644 snap/info_test.go create mode 100644 snap/pack/export_test.go create mode 100644 snap/pack/pack.go create mode 100644 snap/pack/pack_test.go create mode 100644 snap/restartcond.go create mode 100644 snap/restartcond_test.go create mode 100644 snap/revision.go create mode 100644 snap/revision_test.go create mode 100644 snap/seed_yaml.go create mode 100644 snap/seed_yaml_test.go create mode 100644 snap/snapdir/snapdir.go create mode 100644 snap/snapdir/snapdir_test.go create mode 100644 snap/snapenv/snapenv.go create mode 100644 snap/snapenv/snapenv_test.go create mode 100644 snap/snaptest/snaptest.go create mode 100644 snap/snaptest/snaptest_test.go create mode 100644 snap/squashfs/squashfs.go create mode 100644 snap/squashfs/squashfs_test.go create mode 100644 snap/types.go create mode 100644 snap/types_test.go create mode 100644 snap/validate.go create mode 100644 snap/validate_test.go create mode 100644 spdx/licenses.go create mode 100644 spdx/parser.go create mode 100644 spdx/parser_test.go create mode 100644 spdx/scanner.go create mode 100644 spdx/scanner_test.go create mode 100644 spdx/validate.go create mode 100644 spread.yaml create mode 100644 store/auth.go create mode 100644 store/auth_test.go create mode 100644 store/cache.go create mode 100644 store/cache_test.go create mode 100644 store/details.go create mode 100644 store/errors.go create mode 100644 store/export_test.go create mode 100644 store/store.go create mode 100644 store/store_test.go create mode 100644 store/storetest/storetest.go create mode 100644 store/userinfo.go create mode 100644 store/userinfo_test.go create mode 100644 strutil/map.go create mode 100644 strutil/map_test.go create mode 100644 strutil/strutil.go create mode 100644 strutil/strutil_test.go create mode 100644 strutil/version.go create mode 100644 strutil/version_test.go create mode 100644 systemd/escape.go create mode 100644 systemd/escape_test.go create mode 100644 systemd/export_test.go create mode 100644 systemd/sdnotify.go create mode 100644 systemd/sdnotify_test.go create mode 100644 systemd/systemd.go create mode 100644 systemd/systemd_test.go create mode 100644 tests/completion/data/files/a/a_thing.txt create mode 100644 tests/completion/data/files/b/b_thing.txt create mode 100644 tests/completion/data/files/b/c/b_c_thing.txt create mode 100644 tests/completion/data/files/d/d_thing.txt create mode 100644 tests/completion/data/files/thing.txt create mode 100644 tests/completion/data/hosts.txt create mode 100644 tests/completion/data/twisted/.just a hidden file create mode 100644 tests/completion/data/twisted/this is a file with spaces in it.doc create mode 100644 tests/completion/data/twisted/this isn't.innit create mode 100644 tests/completion/dirs.complete create mode 100644 tests/completion/dirs.sh create mode 100644 tests/completion/dirs.vars create mode 100644 tests/completion/files.complete create mode 100644 tests/completion/files.sh create mode 100644 tests/completion/files.vars create mode 100644 tests/completion/func.complete create mode 100644 tests/completion/func.sh create mode 100644 tests/completion/func.vars create mode 100644 tests/completion/funky.complete create mode 100644 tests/completion/funky.sh create mode 100644 tests/completion/funky.vars create mode 100644 tests/completion/funkyfunc.complete create mode 100644 tests/completion/funkyfunc.sh create mode 100644 tests/completion/funkyfunc.vars create mode 100644 tests/completion/hosts.complete create mode 100644 tests/completion/hosts.sh create mode 100644 tests/completion/hosts.vars create mode 100644 tests/completion/hosts_n_dirs.complete create mode 100644 tests/completion/hosts_n_dirs.sh create mode 100644 tests/completion/hosts_n_dirs.vars create mode 100644 tests/completion/indirect/task.exp create mode 100644 tests/completion/indirect/task.yaml create mode 100644 tests/completion/lib.exp0 create mode 100644 tests/completion/plain.complete create mode 100644 tests/completion/plain.sh create mode 100644 tests/completion/plain.vars create mode 100644 tests/completion/plain_plusdirs.complete create mode 100644 tests/completion/plain_plusdirs.sh create mode 100644 tests/completion/plain_plusdirs.vars create mode 100644 tests/completion/simple/task.exp create mode 100644 tests/completion/simple/task.yaml create mode 100644 tests/completion/snippets/task.exp create mode 100644 tests/completion/snippets/task.yaml create mode 100644 tests/completion/twisted.complete create mode 100644 tests/completion/twisted.sh create mode 100644 tests/completion/twisted.vars create mode 100644 tests/external-backend.md create mode 100644 tests/lib/assertions/auto-import.assert create mode 100644 tests/lib/assertions/developer1-my-classic-w-gadget.model create mode 100644 tests/lib/assertions/developer1-my-classic.model create mode 100644 tests/lib/assertions/developer1-pc-w-config.model create mode 100644 tests/lib/assertions/developer1-pc.model create mode 100644 tests/lib/assertions/developer1.account create mode 100644 tests/lib/assertions/developer1.account-key create mode 100644 tests/lib/assertions/fake.store create mode 100644 tests/lib/assertions/nested-amd64.model create mode 100644 tests/lib/assertions/nested-i386.model create mode 100644 tests/lib/assertions/pc-production.model create mode 100644 tests/lib/assertions/pc-staging.model create mode 100644 tests/lib/assertions/pi2.model create mode 100644 tests/lib/assertions/testrootorg-store.account-key create mode 100644 tests/lib/boot.sh create mode 100644 tests/lib/cache/README.txt create mode 100755 tests/lib/changes.sh create mode 100755 tests/lib/dbus.sh create mode 100644 tests/lib/dirs.sh create mode 100755 tests/lib/external/prepare-ssh.sh create mode 100644 tests/lib/fakedevicesvc/main.go create mode 100644 tests/lib/fakestore/cmd/fakestore/cmd_make_refreshable.go create mode 100644 tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go create mode 100644 tests/lib/fakestore/cmd/fakestore/cmd_new_snap_rev.go create mode 100644 tests/lib/fakestore/cmd/fakestore/cmd_run.go create mode 100644 tests/lib/fakestore/cmd/fakestore/main.go create mode 100644 tests/lib/fakestore/refresh/refresh.go create mode 100644 tests/lib/fakestore/refresh/snap_asserts.go create mode 100644 tests/lib/fakestore/store/store.go create mode 100644 tests/lib/fakestore/store/store_test.go create mode 100644 tests/lib/files.sh create mode 100644 tests/lib/list-interfaces.go create mode 100755 tests/lib/mkpinentry.sh create mode 100644 tests/lib/names.sh create mode 100644 tests/lib/nested.sh create mode 100644 tests/lib/network.sh create mode 100644 tests/lib/os-release.16 create mode 100755 tests/lib/pinentry-fake.sh create mode 100755 tests/lib/pkgdb.sh create mode 100755 tests/lib/prepare-restore.sh create mode 100755 tests/lib/prepare.sh create mode 100644 tests/lib/quiet.sh create mode 100644 tests/lib/ramdisk.sh create mode 100755 tests/lib/reset.sh create mode 100644 tests/lib/snaps.sh create mode 100755 tests/lib/snaps/account-control-consumer/bin/chpasswd create mode 100755 tests/lib/snaps/account-control-consumer/bin/deluser create mode 100755 tests/lib/snaps/account-control-consumer/bin/useradd create mode 100644 tests/lib/snaps/account-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/aliases/bin/cmd1 create mode 100755 tests/lib/snaps/aliases/bin/cmd2 create mode 100644 tests/lib/snaps/aliases/meta/icon.png create mode 100644 tests/lib/snaps/aliases/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-desktop/bin/echo create mode 100644 tests/lib/snaps/basic-desktop/meta/gui/echo.desktop create mode 100644 tests/lib/snaps/basic-desktop/meta/gui/icon.png create mode 100644 tests/lib/snaps/basic-desktop/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-hooks/meta/hooks/configure create mode 100755 tests/lib/snaps/basic-hooks/meta/hooks/invalid-hook create mode 100644 tests/lib/snaps/basic-hooks/meta/icon.png create mode 100644 tests/lib/snaps/basic-hooks/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/connect-plug-foo create mode 100755 tests/lib/snaps/basic-iface-hooks-consumer/meta/hooks/prepare-plug-foo create mode 100644 tests/lib/snaps/basic-iface-hooks-consumer/meta/icon.png create mode 100644 tests/lib/snaps/basic-iface-hooks-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/connect-slot-bar create mode 100755 tests/lib/snaps/basic-iface-hooks-producer/meta/hooks/prepare-slot-bar create mode 100644 tests/lib/snaps/basic-iface-hooks-producer/meta/icon.png create mode 100644 tests/lib/snaps/basic-iface-hooks-producer/meta/snap.yaml create mode 100755 tests/lib/snaps/basic-run/bin/echo create mode 100644 tests/lib/snaps/basic-run/meta/snap.yaml create mode 100644 tests/lib/snaps/basic/meta/icon.png create mode 100644 tests/lib/snaps/basic/meta/snap.yaml create mode 100755 tests/lib/snaps/browser-support-consumer/bin/cmd create mode 100644 tests/lib/snaps/browser-support-consumer/meta/snap.yaml.in create mode 100644 tests/lib/snaps/classic-gadget/meta/gadget.yaml create mode 100755 tests/lib/snaps/classic-gadget/meta/hooks/prepare-device create mode 100644 tests/lib/snaps/classic-gadget/meta/icon.png create mode 100644 tests/lib/snaps/classic-gadget/meta/snap.yaml create mode 100755 tests/lib/snaps/config-versions-v2/meta/hooks/configure create mode 100644 tests/lib/snaps/config-versions-v2/meta/icon.png create mode 100644 tests/lib/snaps/config-versions-v2/meta/snap.yaml create mode 100755 tests/lib/snaps/config-versions/meta/hooks/configure create mode 100644 tests/lib/snaps/config-versions/meta/icon.png create mode 100644 tests/lib/snaps/config-versions/meta/snap.yaml create mode 100755 tests/lib/snaps/data-writer/bin/write-data create mode 100644 tests/lib/snaps/data-writer/meta/icon.png create mode 100644 tests/lib/snaps/data-writer/meta/snap.yaml create mode 100755 tests/lib/snaps/failing-config-hooks/meta/hooks/configure create mode 100644 tests/lib/snaps/failing-config-hooks/meta/icon.png create mode 100644 tests/lib/snaps/failing-config-hooks/meta/snap.yaml create mode 100755 tests/lib/snaps/firewall-control-consumer/bin/consumer create mode 100644 tests/lib/snaps/firewall-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/generic-consumer/bin/cmd create mode 100644 tests/lib/snaps/generic-consumer/meta/snap.yaml.in create mode 100755 tests/lib/snaps/gpio-consumer/bin/read create mode 100644 tests/lib/snaps/gpio-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/hardware-observe-consumer/bin/consumer create mode 100644 tests/lib/snaps/hardware-observe-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/home-consumer/bin/reader create mode 100755 tests/lib/snaps/home-consumer/bin/writer create mode 100644 tests/lib/snaps/home-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/iio-consumer/bin/read create mode 100755 tests/lib/snaps/iio-consumer/bin/write create mode 100644 tests/lib/snaps/iio-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/locale-control-consumer/bin/get create mode 100755 tests/lib/snaps/locale-control-consumer/bin/set create mode 100644 tests/lib/snaps/locale-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/log-observe-consumer/bin/cmd create mode 100755 tests/lib/snaps/log-observe-consumer/bin/consumer create mode 100644 tests/lib/snaps/log-observe-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/modem-manager-consumer/bin/consumer create mode 100644 tests/lib/snaps/modem-manager-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/mount-observe-consumer/bin/consumer create mode 100644 tests/lib/snaps/mount-observe-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/network-bind-consumer/bin/consumer create mode 100644 tests/lib/snaps/network-bind-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/network-consumer/bin/consumer create mode 100644 tests/lib/snaps/network-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/network-control-consumer/bin/cmd create mode 100644 tests/lib/snaps/network-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/network-observe-consumer/bin/consumer create mode 100644 tests/lib/snaps/network-observe-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/process-control-consumer/bin/signal create mode 100644 tests/lib/snaps/process-control-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/shutdown-introspection-consumer/bin/consumer create mode 100644 tests/lib/snaps/shutdown-introspection-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/configure create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/install create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/post-refresh create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/pre-refresh create mode 100755 tests/lib/snaps/snap-hooks/meta/hooks/remove create mode 100644 tests/lib/snaps/snap-hooks/meta/snap.yaml create mode 100755 tests/lib/snaps/snap-install-hook-broken/meta/hooks/install create mode 100644 tests/lib/snaps/snap-install-hook-broken/meta/snap.yaml create mode 100755 tests/lib/snaps/snapctl-from-snap/bin/snapctl-get create mode 100755 tests/lib/snaps/snapctl-from-snap/bin/snapctl-set create mode 100755 tests/lib/snaps/snapctl-from-snap/meta/hooks/configure create mode 100644 tests/lib/snaps/snapctl-from-snap/meta/snap.yaml create mode 100755 tests/lib/snaps/snapctl-hooks-v2/meta/hooks/configure create mode 100644 tests/lib/snaps/snapctl-hooks-v2/meta/icon.png create mode 100644 tests/lib/snaps/snapctl-hooks-v2/meta/snap.yaml create mode 100755 tests/lib/snaps/snapctl-hooks/meta/hooks/configure create mode 100644 tests/lib/snaps/snapctl-hooks/meta/icon.png create mode 100644 tests/lib/snaps/snapctl-hooks/meta/snap.yaml create mode 100755 tests/lib/snaps/socket-activation/bin/sleep create mode 100644 tests/lib/snaps/socket-activation/meta/snap.yaml create mode 100755 tests/lib/snaps/test-classic-cgroup/bin/read-fb create mode 100755 tests/lib/snaps/test-classic-cgroup/bin/read-kmsg create mode 100644 tests/lib/snaps/test-classic-cgroup/meta/snap.yaml create mode 100755 tests/lib/snaps/test-devmode-cgroup/bin/read-fb create mode 100755 tests/lib/snaps/test-devmode-cgroup/bin/read-kmsg create mode 100644 tests/lib/snaps/test-devmode-cgroup/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-auto-aliases/bin/wellknown1 create mode 100755 tests/lib/snaps/test-snapd-auto-aliases/bin/wellknown2 create mode 100644 tests/lib/snaps/test-snapd-auto-aliases/meta/icon.png create mode 100644 tests/lib/snaps/test-snapd-auto-aliases/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-autopilot-consumer/consumer create mode 100644 tests/lib/snaps/test-snapd-autopilot-consumer/provider.py create mode 100644 tests/lib/snaps/test-snapd-autopilot-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-autopilot-consumer/wrapper create mode 100644 tests/lib/snaps/test-snapd-base-bare/Makefile create mode 100644 tests/lib/snaps/test-snapd-base-bare/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-base/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-base/random-file create mode 100644 tests/lib/snaps/test-snapd-busybox-static/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-check-fs-access/bin/read-dir create mode 100755 tests/lib/snaps/test-snapd-check-fs-access/bin/read-file create mode 100755 tests/lib/snaps/test-snapd-check-fs-access/bin/write-dir create mode 100755 tests/lib/snaps/test-snapd-check-fs-access/bin/write-file create mode 100644 tests/lib/snaps/test-snapd-check-fs-access/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-classic-confinement/bin/classic-confinement create mode 100755 tests/lib/snaps/test-snapd-classic-confinement/bin/recurse create mode 100644 tests/lib/snaps/test-snapd-classic-confinement/meta/icon.png create mode 100644 tests/lib/snaps/test-snapd-classic-confinement/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-complexion/bin/test-snapd-complexion create mode 100644 tests/lib/snaps/test-snapd-complexion/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-complexion/test-snapd-complexion.bash-completer create mode 100644 tests/lib/snaps/test-snapd-content-advanced-plug/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-advanced-slot/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-advanced-slot/source/canary create mode 100755 tests/lib/snaps/test-snapd-content-plug-empty-content-attr/bin/content-plug create mode 100644 tests/lib/snaps/test-snapd-content-plug-empty-content-attr/import/.placeholder create mode 100644 tests/lib/snaps/test-snapd-content-plug-empty-content-attr/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-content-plug/bin/content-plug create mode 100644 tests/lib/snaps/test-snapd-content-plug/import/.placeholder create mode 100644 tests/lib/snaps/test-snapd-content-plug/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-slot-empty-content-attr/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-slot-empty-content-attr/shared-content create mode 100644 tests/lib/snaps/test-snapd-content-slot/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-content-slot/shared-content create mode 100755 tests/lib/snaps/test-snapd-control-consumer/bin/install create mode 100755 tests/lib/snaps/test-snapd-control-consumer/bin/list create mode 100644 tests/lib/snaps/test-snapd-control-consumer/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-cups-control-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-dbus-consumer/consumer.py create mode 100644 tests/lib/snaps/test-snapd-dbus-consumer/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-dbus-provider/provider.py create mode 100644 tests/lib/snaps/test-snapd-dbus-provider/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-dbus-provider/wrapper create mode 100755 tests/lib/snaps/test-snapd-desktop/bin/check-dirs create mode 100755 tests/lib/snaps/test-snapd-desktop/bin/check-files create mode 100644 tests/lib/snaps/test-snapd-desktop/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-devmode/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-devpts/bin/openpty create mode 100755 tests/lib/snaps/test-snapd-devpts/bin/useptmx create mode 100644 tests/lib/snaps/test-snapd-devpts/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-framebuffer/bin/read create mode 100755 tests/lib/snaps/test-snapd-framebuffer/bin/write create mode 100644 tests/lib/snaps/test-snapd-framebuffer/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-fuse-consumer/Makefile create mode 100644 tests/lib/snaps/test-snapd-fuse-consumer/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-go-webserver/main.go create mode 100644 tests/lib/snaps/test-snapd-go-webserver/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-hardware-random-control/bin/check create mode 100644 tests/lib/snaps/test-snapd-hardware-random-control/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-kernel-module-control-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-down create mode 100755 tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-up create mode 100644 tests/lib/snaps/test-snapd-libvirt-consumer/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-libvirt-consumer/vm/ping-unikernel.xml create mode 100755 tests/lib/snaps/test-snapd-multi-service/bin/start create mode 100644 tests/lib/snaps/test-snapd-multi-service/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-openvswitch-consumer/bin/ovs-vsctl create mode 100644 tests/lib/snaps/test-snapd-openvswitch-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-password-manager-service-consumer/bin/secret-tool create mode 100644 tests/lib/snaps/test-snapd-password-manager-service-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-physical-memory-observe/bin/head-mem create mode 100644 tests/lib/snaps/test-snapd-physical-memory-observe/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-policy-app-consumer/bin/run create mode 100644 tests/lib/snaps/test-snapd-policy-app-consumer/meta/gui/test-desktop.desktop create mode 100644 tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-policy-app-provider-classic/bin/run create mode 100644 tests/lib/snaps/test-snapd-policy-app-provider-classic/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-policy-app-provider-core/bin/run create mode 100644 tests/lib/snaps/test-snapd-policy-app-provider-core/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-private/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-python-webserver/index.html create mode 100755 tests/lib/snaps/test-snapd-python-webserver/server.py create mode 100644 tests/lib/snaps/test-snapd-python-webserver/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-requires-base/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service-try-v1/bin/service create mode 100644 tests/lib/snaps/test-snapd-service-try-v1/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service-try-v2/bin/service create mode 100644 tests/lib/snaps/test-snapd-service-try-v2/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service-v1-good/bin/good create mode 100644 tests/lib/snaps/test-snapd-service-v1-good/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service-v2-bad/bin/bad create mode 100644 tests/lib/snaps/test-snapd-service-v2-bad/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-service/bin/reload create mode 100755 tests/lib/snaps/test-snapd-service/bin/start create mode 100755 tests/lib/snaps/test-snapd-service/bin/start-other create mode 100755 tests/lib/snaps/test-snapd-service/meta/hooks/configure create mode 100644 tests/lib/snaps/test-snapd-service/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-sh/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-system-observe-consumer/consumer.py create mode 100755 tests/lib/snaps/test-snapd-system-observe-consumer/dbus-introspect.py create mode 100644 tests/lib/snaps/test-snapd-system-observe-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-tools/bin/block create mode 100755 tests/lib/snaps/test-snapd-tools/bin/cat create mode 100755 tests/lib/snaps/test-snapd-tools/bin/cmd create mode 100755 tests/lib/snaps/test-snapd-tools/bin/echo create mode 100755 tests/lib/snaps/test-snapd-tools/bin/env create mode 100755 tests/lib/snaps/test-snapd-tools/bin/fail create mode 100755 tests/lib/snaps/test-snapd-tools/bin/head create mode 100755 tests/lib/snaps/test-snapd-tools/bin/sh create mode 100755 tests/lib/snaps/test-snapd-tools/bin/success create mode 100644 tests/lib/snaps/test-snapd-tools/meta/icon.png create mode 100644 tests/lib/snaps/test-snapd-tools/meta/snap.yaml create mode 100755 tests/lib/snaps/test-snapd-tuntap/bin/tuntap.py create mode 100644 tests/lib/snaps/test-snapd-tuntap/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-uhid/Makefile create mode 100644 tests/lib/snaps/test-snapd-uhid/snapcraft.yaml create mode 100644 tests/lib/snaps/test-snapd-uhid/uhid-test.c create mode 100644 tests/lib/snaps/test-snapd-unknown-interfaces/meta/snap.yaml create mode 100644 tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml create mode 100755 tests/lib/snaps/test-snapd-with-configure/meta/hooks/configure create mode 100644 tests/lib/snaps/test-snapd-with-configure/meta/snap.yaml create mode 100755 tests/lib/snaps/test-strict-cgroup/bin/read-fb create mode 100755 tests/lib/snaps/test-strict-cgroup/bin/read-kmsg create mode 100644 tests/lib/snaps/test-strict-cgroup/meta/snap.yaml create mode 100755 tests/lib/snaps/time-control-consumer/bin/read create mode 100755 tests/lib/snaps/time-control-consumer/bin/timedatectl create mode 100755 tests/lib/snaps/time-control-consumer/bin/write create mode 100644 tests/lib/snaps/time-control-consumer/meta/snap.yaml create mode 100644 tests/lib/store.sh create mode 100644 tests/lib/strings.sh create mode 100644 tests/lib/systemd-escape/main.go create mode 100644 tests/lib/systemd.sh create mode 100644 tests/main/abort/task.yaml create mode 100644 tests/main/ack/alice.account create mode 100644 tests/main/ack/alice.account-key create mode 100644 tests/main/ack/bob.assertions create mode 100644 tests/main/ack/task.yaml create mode 100644 tests/main/alias/task.yaml create mode 100644 tests/main/auth-errors/task.yaml create mode 100644 tests/main/auto-aliases/task.yaml create mode 100644 tests/main/auto-refresh/task.yaml create mode 100644 tests/main/base-snaps/task.yaml create mode 100644 tests/main/canonical-livepatch/task.yaml create mode 100644 tests/main/catalog-update/task.yaml create mode 100644 tests/main/cgroup-freezer/task.yaml create mode 100644 tests/main/change-errors/task.yaml create mode 100644 tests/main/chattr/task.yaml create mode 100644 tests/main/chattr/toggle.go create mode 100644 tests/main/classic-confinement-not-supported/task.yaml create mode 100644 tests/main/classic-confinement/task.yaml create mode 100644 tests/main/classic-custom-device-reg/task.yaml create mode 100644 tests/main/classic-firstboot/task.yaml create mode 100644 tests/main/classic-ubuntu-core-transition-auth/task.yaml create mode 100644 tests/main/classic-ubuntu-core-transition-two-cores/task.yaml create mode 100644 tests/main/classic-ubuntu-core-transition/task.yaml create mode 100644 tests/main/cmdline/task.yaml create mode 100644 tests/main/completion/abort.exp create mode 100644 tests/main/completion/ack.exp create mode 100644 tests/main/completion/alias.exp create mode 100644 tests/main/completion/buy.exp create mode 100644 tests/main/completion/change.exp create mode 100644 tests/main/completion/delete-key.exp create mode 100644 tests/main/completion/disable.exp create mode 100644 tests/main/completion/download.exp create mode 100644 tests/main/completion/enable.exp create mode 100644 tests/main/completion/export-key.exp create mode 100644 tests/main/completion/get.exp create mode 100644 tests/main/completion/info.exp create mode 100644 tests/main/completion/install.exp create mode 100644 tests/main/completion/key.exp0 create mode 120000 tests/main/completion/lib.exp0 create mode 100644 tests/main/completion/list.exp create mode 100644 tests/main/completion/refresh.exp create mode 100644 tests/main/completion/remove.exp create mode 100644 tests/main/completion/revert.exp create mode 100644 tests/main/completion/set.exp create mode 100644 tests/main/completion/sign-build.exp create mode 100644 tests/main/completion/sign.exp create mode 100644 tests/main/completion/task.yaml create mode 100644 tests/main/completion/toplevel.exp create mode 100644 tests/main/completion/try.exp create mode 100644 tests/main/completion/watch.exp create mode 100644 tests/main/config-versions/task.yaml create mode 100644 tests/main/confinement-classic/task.yaml create mode 100644 tests/main/confinement-classic/test-snapd-hello-classic/Makefile create mode 100644 tests/main/confinement-classic/test-snapd-hello-classic/test-snapd-hello-classic.c create mode 100644 tests/main/core-snap-not-test-test/task.yaml create mode 100644 tests/main/core-snap-refresh-on-core/task.yaml create mode 100644 tests/main/core-snap-refresh/task.yaml create mode 100644 tests/main/create-key/passphrase_mismatch.exp create mode 100644 tests/main/create-key/successful_default.exp create mode 100644 tests/main/create-key/successful_non_default.exp create mode 100644 tests/main/create-key/task.yaml create mode 100644 tests/main/create-user/task.yaml create mode 100644 tests/main/debs-have-built-using/task.yaml create mode 100644 tests/main/debug-confinement/task.yaml create mode 100644 tests/main/dirs-not-shared-with-host/task.yaml create mode 100644 tests/main/econnreset/task.yaml create mode 100644 tests/main/enable-disable-units-gpio/task.yaml create mode 100644 tests/main/enable-disable/task.yaml create mode 100644 tests/main/failover/task.yaml create mode 100644 tests/main/fakestore-install/task.yaml create mode 100644 tests/main/find-private/successful_login.exp create mode 100644 tests/main/find-private/task.yaml create mode 100644 tests/main/generic-classic-reg/task.yaml create mode 100644 tests/main/help/task.yaml create mode 100644 tests/main/i18n/task.yaml create mode 100644 tests/main/install-cache/task.yaml create mode 100644 tests/main/install-errors/task.yaml create mode 100644 tests/main/install-refresh-remove-hooks/task.yaml create mode 100644 tests/main/install-remove-multi/task.yaml create mode 100644 tests/main/install-sideload/task.yaml create mode 100644 tests/main/install-snaps/task.yaml create mode 100644 tests/main/install-socket-activation/task.yaml create mode 100644 tests/main/install-store-laaaarge/task.yaml create mode 100644 tests/main/install-store/task.yaml create mode 100644 tests/main/interfaces-account-control/task.yaml create mode 100644 tests/main/interfaces-alsa/task.yaml create mode 100644 tests/main/interfaces-autopilot-introspection/task.yaml create mode 100644 tests/main/interfaces-avahi-observe/task.yaml create mode 100644 tests/main/interfaces-bluetooth-control/task.yaml create mode 100644 tests/main/interfaces-bluez/task.yaml create mode 100644 tests/main/interfaces-browser-support/task.yaml create mode 100644 tests/main/interfaces-cli/task.yaml create mode 100644 tests/main/interfaces-content-empty-content-attr/task.yaml create mode 100644 tests/main/interfaces-content-mkdir-writable/task.yaml create mode 100644 tests/main/interfaces-content/task.yaml create mode 100644 tests/main/interfaces-cups-control/task.yaml create mode 100644 tests/main/interfaces-dbus/task.yaml create mode 100644 tests/main/interfaces-desktop/task.yaml create mode 100644 tests/main/interfaces-firewall-control/task.yaml create mode 100644 tests/main/interfaces-framebuffer/task.yaml create mode 100644 tests/main/interfaces-fuse_support/task.yaml create mode 100644 tests/main/interfaces-hardware-observe/task.yaml create mode 100644 tests/main/interfaces-hardware-random-control/task.yaml create mode 100644 tests/main/interfaces-home/task.yaml create mode 100644 tests/main/interfaces-hooks/task.yaml create mode 100644 tests/main/interfaces-iio/task.yaml create mode 100644 tests/main/interfaces-kernel-module-control/task.yaml create mode 100644 tests/main/interfaces-libvirt/task.yaml create mode 100644 tests/main/interfaces-locale-control/task.yaml create mode 100644 tests/main/interfaces-log-observe/task.yaml create mode 100644 tests/main/interfaces-many/task.yaml create mode 100644 tests/main/interfaces-mount-observe/task.yaml create mode 100644 tests/main/interfaces-network-bind/task.yaml create mode 100644 tests/main/interfaces-network-control-ip-netns/task.yaml create mode 100644 tests/main/interfaces-network-control-tuntap/task.yaml create mode 100644 tests/main/interfaces-network-control/task.yaml create mode 100644 tests/main/interfaces-network-manager/task.yaml create mode 100644 tests/main/interfaces-network-observe/task.yaml create mode 100644 tests/main/interfaces-network-setup-control/task.yaml create mode 100644 tests/main/interfaces-network-setup-observe/task.yaml create mode 100644 tests/main/interfaces-network/task.yaml create mode 100644 tests/main/interfaces-openvswitch/task.yaml create mode 100644 tests/main/interfaces-password-manager-service/task.yaml create mode 100644 tests/main/interfaces-physical-memory-observe/task.yaml create mode 100644 tests/main/interfaces-process-control/task.yaml create mode 100644 tests/main/interfaces-shutdown-introspection/task.yaml create mode 100644 tests/main/interfaces-snapd-control-with-manage/task.yaml create mode 100644 tests/main/interfaces-snapd-control/task.yaml create mode 100644 tests/main/interfaces-system-observe/task.yaml create mode 100644 tests/main/interfaces-time-control/task.yaml create mode 100644 tests/main/interfaces-udev/task.yaml create mode 100644 tests/main/interfaces-uhid/task.yaml create mode 100644 tests/main/interfaces-upower-observe/task.yaml create mode 100644 tests/main/interfaces-wayland/task.yaml create mode 100644 tests/main/known-remote/task.yaml create mode 100644 tests/main/known/task.yaml create mode 100644 tests/main/listing/task.yaml create mode 100644 tests/main/local-install-w-metadata/digest.go create mode 100644 tests/main/local-install-w-metadata/task.yaml create mode 100644 tests/main/login/missing_email_error.exp create mode 100644 tests/main/login/successful_login.exp create mode 100644 tests/main/login/task.yaml create mode 100644 tests/main/login/unsuccessful_login.exp create mode 100644 tests/main/lxd/task.yaml create mode 100644 tests/main/manpages/task.yaml create mode 100644 tests/main/media-sharing/task.yaml create mode 100644 tests/main/nfs-support/task.yaml create mode 100644 tests/main/op-install-failed-undone/task.yaml create mode 100644 tests/main/op-remove-retry/task.yaml create mode 100644 tests/main/op-remove/task.yaml create mode 100644 tests/main/postrm-purge/task.yaml create mode 100644 tests/main/prefer/task.yaml create mode 100644 tests/main/prepare-image-grub/task.yaml create mode 100644 tests/main/prepare-image-uboot/task.yaml create mode 100644 tests/main/refresh-all-undo/task.yaml create mode 100644 tests/main/refresh-all/task.yaml create mode 100644 tests/main/refresh-delta-from-core/task.yaml create mode 100644 tests/main/refresh-delta/task.yaml create mode 100644 tests/main/refresh-devmode/task.yaml create mode 100644 tests/main/refresh-undo/task.yaml create mode 100644 tests/main/refresh/task.yaml create mode 100644 tests/main/regression-home-snap-root-owned/task.yaml create mode 100644 tests/main/remove-errors/task.yaml create mode 100644 tests/main/revert-devmode/task.yaml create mode 100644 tests/main/revert-sideload/task.yaml create mode 100644 tests/main/revert/task.yaml create mode 100644 tests/main/searching/task.yaml create mode 100644 tests/main/security-apparmor/task.yaml create mode 100644 tests/main/security-device-cgroups-classic/task.yaml create mode 100644 tests/main/security-device-cgroups-devmode/task.yaml create mode 100644 tests/main/security-device-cgroups-jailmode/task.yaml create mode 100644 tests/main/security-device-cgroups-serial-port/task.yaml create mode 100644 tests/main/security-device-cgroups-strict/task.yaml create mode 100644 tests/main/security-device-cgroups/task.yaml create mode 100644 tests/main/security-devpts/task.yaml create mode 100644 tests/main/security-private-tmp/task.yaml create mode 100644 tests/main/security-private-tmp/tmp-create.exp create mode 100644 tests/main/security-profiles/task.yaml create mode 100644 tests/main/security-setuid-root/task.yaml create mode 100644 tests/main/server-snap/task.yaml create mode 100644 tests/main/set-proxy-store/task.yaml create mode 100644 tests/main/snap-auto-import-asserts-spools/task.yaml create mode 100644 tests/main/snap-auto-import-asserts/task.yaml create mode 100644 tests/main/snap-auto-mount/task.yaml create mode 100644 tests/main/snap-confine-from-core/task.yaml create mode 100644 tests/main/snap-confine-privs/task.yaml create mode 100644 tests/main/snap-confine-privs/uids-and-gids.c create mode 100644 tests/main/snap-confine/task.yaml create mode 100644 tests/main/snap-connect/task.yaml create mode 100644 tests/main/snap-debug-get-base-declaration/task.yaml create mode 100644 tests/main/snap-discard-ns/task.yaml create mode 100644 tests/main/snap-disconnect/task.yaml create mode 100644 tests/main/snap-download/task.yaml create mode 100644 tests/main/snap-env/task.yaml create mode 100644 tests/main/snap-get/task.yaml create mode 100644 tests/main/snap-info/check.py create mode 100644 tests/main/snap-info/task.yaml create mode 100644 tests/main/snap-interface/snap-interface-core-support.yaml create mode 100644 tests/main/snap-interface/task.yaml create mode 100644 tests/main/snap-multi-service-failing/task.yaml create mode 100644 tests/main/snap-on-non-shared-root/task.yaml create mode 100644 tests/main/snap-readme/task.yaml create mode 100644 tests/main/snap-remove-not-mounted/task.yaml create mode 100644 tests/main/snap-repair/task.yaml create mode 100644 tests/main/snap-run-alias/task.yaml create mode 100644 tests/main/snap-run-hook/task.yaml create mode 100644 tests/main/snap-run-symlink-error/task.yaml create mode 100644 tests/main/snap-run-symlink/task.yaml create mode 100644 tests/main/snap-run-userdata-current/task.yaml create mode 100644 tests/main/snap-run/task.yaml create mode 100644 tests/main/snap-seccomp/task.yaml create mode 100644 tests/main/snap-service/task.yaml create mode 100644 tests/main/snap-set-core-w-no-core/task.yaml create mode 100644 tests/main/snap-set/task.yaml create mode 100644 tests/main/snap-sign/create-key.exp create mode 100644 tests/main/snap-sign/sign-model.exp create mode 100644 tests/main/snap-sign/task.yaml create mode 100644 tests/main/snap-switch/task.yaml create mode 100644 tests/main/snap-update-ns/task.yaml create mode 100644 tests/main/snap-userd-reexec/task.yaml create mode 100644 tests/main/snap-userd/task.yaml create mode 100644 tests/main/snapctl-configure-core/task.yaml create mode 100644 tests/main/snapctl-from-snap/task.yaml create mode 100644 tests/main/snapctl-services/task.yaml create mode 100644 tests/main/snapctl/task.yaml create mode 100644 tests/main/snapd-notify/task.yaml create mode 100644 tests/main/snapd-reexec/task.yaml create mode 100644 tests/main/static/task.yaml create mode 100644 tests/main/systemd-service/task.yaml create mode 100644 tests/main/try-non-fatal/task.yaml create mode 100644 tests/main/try-snap-goes-away/task.yaml create mode 100644 tests/main/try-snap-is-optional/task.yaml create mode 100644 tests/main/try-twice-with-daemon/task.yaml create mode 100644 tests/main/try/task.yaml create mode 100644 tests/main/ubuntu-core-apt/task.yaml create mode 100644 tests/main/ubuntu-core-classic/task.yaml create mode 100644 tests/main/ubuntu-core-create-user/task.yaml create mode 100644 tests/main/ubuntu-core-custom-device-reg-extras/manip_seed.py create mode 100755 tests/main/ubuntu-core-custom-device-reg-extras/prepare-device create mode 100644 tests/main/ubuntu-core-custom-device-reg-extras/task.yaml create mode 100644 tests/main/ubuntu-core-custom-device-reg/manip_seed.py create mode 100755 tests/main/ubuntu-core-custom-device-reg/prepare-device create mode 100644 tests/main/ubuntu-core-custom-device-reg/task.yaml create mode 100644 tests/main/ubuntu-core-device-reg/task.yaml create mode 100644 tests/main/ubuntu-core-fan/task.yaml create mode 100644 tests/main/ubuntu-core-gadget-config-defaults/manip_seed.py create mode 100644 tests/main/ubuntu-core-gadget-config-defaults/task.yaml create mode 100644 tests/main/ubuntu-core-grub/task.yaml create mode 100644 tests/main/ubuntu-core-os-release/task.yaml create mode 100644 tests/main/ubuntu-core-reboot/task.yaml create mode 100644 tests/main/ubuntu-core-services/task.yaml create mode 100644 tests/main/ubuntu-core-uboot/task.yaml create mode 100644 tests/main/ubuntu-core-upgrade/task.yaml create mode 100644 tests/main/ubuntu-core-writablepaths/task.yaml create mode 100644 tests/main/user-data-handling/task.yaml create mode 100644 tests/main/whoami/successful_login.exp create mode 100644 tests/main/whoami/task.yaml create mode 100644 tests/main/writable-areas/task.yaml create mode 100644 tests/main/xauth-migration/task.yaml create mode 100644 tests/main/xdg-open-compat/task.yaml create mode 100644 tests/manual-tests.md create mode 100644 tests/nested/core-revert/task.yaml create mode 100644 tests/nested/extra-snaps-assertions/task.yaml create mode 100644 tests/nested/image-build/task.yaml create mode 100644 tests/nightly/docker/task.yaml create mode 100644 tests/nightly/unity/task.yaml create mode 100644 tests/regression/lp-1595444/task.yaml create mode 100644 tests/regression/lp-1597839/task.yaml create mode 100644 tests/regression/lp-1597842/task.yaml create mode 100644 tests/regression/lp-1599891/task.yaml create mode 100644 tests/regression/lp-1606277/task.yaml create mode 100644 tests/regression/lp-1607796/task.yaml create mode 100644 tests/regression/lp-1613845/task.yaml create mode 100644 tests/regression/lp-1615113/task.yaml create mode 100644 tests/regression/lp-1618683/task.yaml create mode 100644 tests/regression/lp-1630479/task.yaml create mode 100644 tests/regression/lp-1641885/task.yaml create mode 100644 tests/regression/lp-1644439/task.yaml create mode 100644 tests/regression/lp-1665004/task.yaml create mode 100644 tests/regression/lp-1667385/task.yaml create mode 100644 tests/regression/lp-1693042/task.yaml create mode 100755 tests/regression/lp-1704860/snap-env-query.sh create mode 100644 tests/regression/lp-1704860/task.yaml create mode 100644 tests/regression/lp-1732555/task.yaml create mode 100644 tests/unit/c-unit-tests-clang/task.yaml create mode 100644 tests/unit/c-unit-tests-gcc/task.yaml create mode 100644 tests/unit/gccgo/task.yaml create mode 100644 tests/unit/go/task.yaml create mode 100644 tests/upgrade/basic/task.yaml create mode 100644 tests/upgrade/snapd-xdg-open/task.yaml create mode 100755 tests/util/benchmark.sh create mode 100644 testutil/base.go create mode 100644 testutil/checkers.go create mode 100644 testutil/checkers_test.go create mode 100644 testutil/dbustest.go create mode 100644 testutil/exec.go create mode 100644 testutil/exec_test.go create mode 100644 timeout/timeout.go create mode 100644 timeout/timeout_test.go create mode 100644 timeutil/export_test.go create mode 100644 timeutil/schedule.go create mode 100644 timeutil/schedule_test.go create mode 100755 update-pot create mode 100644 userd/launcher.go create mode 100644 userd/launcher_test.go create mode 100644 userd/userd.go create mode 100644 vendor/vendor.json create mode 100644 wrappers/binaries.go create mode 100644 wrappers/binaries_test.go create mode 100644 wrappers/desktop.go create mode 100644 wrappers/desktop_test.go create mode 100644 wrappers/export_test.go create mode 100644 wrappers/services.go create mode 100644 wrappers/services_gen_test.go create mode 100644 wrappers/services_test.go create mode 100644 x11/xauth.go create mode 100644 x11/xauth_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1b877f16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +share +tags +.coverage +cmd/version_generated.go +cmd/VERSION +*~ +*.swp +vendor/*/ +.spread-reuse*.yaml +po/snappy.pot + +# snap-confine bits +*.a +*.o +*~ +.*.swp +.*.swp +.dirstamp +cmd/decode-mount-opts/decode-mount-opts +cmd/libsnap-confine-private/unit-tests +cmd/snap-confine/snap-confine +cmd/snap-confine/snap-confine-debug +cmd/snap-confine/snap-confine.apparmor +cmd/snap-confine/unit-tests +cmd/snap-discard-ns/snap-discard-ns +cmd/snap-update-ns/snap-update-ns +cmd/snap-update-ns/unit-tests +cmd/system-shutdown/system-shutdown +cmd/system-shutdown/unit-tests + +# manual pages +cmd/*/*.[1-9] + +# auto-generated systemd units +data/systemd/*.service + +# auto-generated dbus services +data/dbus/*.service + +data/info +data/env/snapd.sh + +# test-driver +*.log +*.trs + +# Automake for the cmd/ parts +cmd/Makefile +cmd/Makefile.in +snap-confine-*.tar.gz +.deps + +# Autoconf +cmd/aclocal.m4 +cmd/autom4te.cache +cmd/compile +cmd/config.guess +cmd/config.h +cmd/config.h.in +cmd/config.status +cmd/config.sub +cmd/configure +cmd/depcomp +cmd/install-sh +cmd/missing +cmd/stamp-h1 +cmd/test-driver diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..570eb1f0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: go +go: + - 1.6 + +env: + global: + # SPREAD_LINODE_KEY + - secure: "bzALrfNSLwM0bjceal1PU5rFErvqVhi00Sygx8jruo6htpZay3hrC2sHCKCQKPn1kvCfHidrHX1vnomg5N+B9o25GZEYSjKSGxuvdNDfCZYqPNjMbz5y7xXYfKWgyo+xtrKRM85Nqy121SfRz3KLDvrOLwwreb+pZv8DG1WraFTd7D6rK7nLnnYNUyw665XBMFVnM8ue3Zu9496Ih/TfQXhnNpsZY8xFWte4+cH7JvVCVTs8snjoGVZi3972PzinNkfBgJa24cUzxFMfiN/AwSBXJQKdVv+FsbB4uRgXAqTNwuus7PptiPNxpWWojuhm1Qgbk0XhGIdJxyUYkmNA4UrZ3C29nIRWbuAiHJ6ZWd1ur3dqphqOcgFInltSHkpfEdlL3YK4dCa2SmJESzotUGnyowCUUCXkWdDaZmFTwyK0Y6He9oyXDK5f+/U7SFlPvok0caJCvB9HbTQR1kYdh048I/R+Ht5QrFOZPk21DYWDOYhn7SzthBDZLsaL6n5gX7Y547SsL4B35YVbpaeHzccG6Mox8rI4bqlGFvP1U5i8uXD4uQjJChlVxpmozUEMok9T5RVediJs540p5uc8DQl48Nke02tXzC/XpGAvpnXT7eiiRNW67zOj2QcIV+ni3lBj3HvZeB9cgjzLNrZSl/t9vseqnNwQWpl3V6nd/bU=" + +git: + quiet: true + +install: + - true + +script: + - ./run-checks --spread + +addons: + apt: + packages: + - xdelta3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..712c273e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +Before contributing you should sign [Canonical's contributor agreement][1], +it’s the easiest way for you to give us permission to use your contributions. + +## Pull Requests and tests + +We need to verify that the code functionality and quality is not degraded +by additions before merging any changes to snapd's codebase. For each PR +we run checks in three different groups: static, unit and spread. + +Static test use several code analysis tools present in the GoLang ecosystem +(go vet, go lint and go fmt) to make sure that the code always aligns with +the standards. They also check the markdown format of documentation files. +All the existing unit tests are also executed, and the coverage info is +reported to coveralls. Regarding [spread](https://github.com/snapcore/spread), +we use it to verify the integrity of the product exercising it as a whole, +both from an end user standpoint (eg. all kind of interactions with the +snap tool from the command line) and from a more systemic approach (testing +upgrades, for instance). + +We do not set as a requirement the addition of spread and unit tests for a PR +to be merged, but encourage the contributors to add them so that the expected +behaviour is explained and verified through the tests and the review process +can be made on the solid base of a working system after the addition of the +changes. If any tests need to be added for a PR to be merged it will be denoted +during the review process. + +[1]: http://www.ubuntu.com/legal/contributors diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, 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 +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 00000000..798e7aae --- /dev/null +++ b/HACKING.md @@ -0,0 +1,200 @@ +# Hacking on snapd + +Hacking on snapd is fun and straightfoward. The code is extensively +unit tested and we use the [spread](https://github.com/snapcore/spread) +integration test framework for the integration/system level tests. + +## Development + +### Setting up a GOPATH + +When working with the source of Go programs, you should define a path within +your home directory (or other workspace) which will be your `GOPATH`. `GOPATH` +is similar to Java's `CLASSPATH` or Python's `~/.local`. `GOPATH` is documented +[online](http://golang.org/pkg/go/build/) and inside the go tool itself + + go help gopath + +Various conventions exist for naming the location of your `GOPATH`, but it +should exist, and be writable by you. For example + + export GOPATH=${HOME}/work + mkdir $GOPATH + +will define and create `$HOME/work` as your local `GOPATH`. The `go` tool +itself will create three subdirectories inside your `GOPATH` when required; +`src`, `pkg` and `bin`, which hold the source of Go programs, compiled packages +and compiled binaries, respectively. + +Setting `GOPATH` correctly is critical when developing Go programs. Set and +export it as part of your login script. + +Add `$GOPATH/bin` to your `PATH`, so you can run the go programs you install: + + PATH="$PATH:$GOPATH/bin" + +(note `$GOPATH` can actually point to multiple locations, like `$PATH`, so if +your `$GOPATH` is more complex than a single entry you'll need to adjust the +above). + +### Getting the snapd sources + +The easiest way to get the source for `snapd` is to use the `go get` command. + + go get -d -v github.com/snapcore/snapd/... + +This command will checkout the source of `snapd` and inspect it for any unmet +Go package dependencies, downloading those as well. `go get` will also build +and install `snapd` and its dependencies. To also build and install `snapd` +itself into `$GOPATH/bin`, omit the `-d` flag. More details on the `go get` +flags are available using + + go help get + +At this point you will have the git local repository of the `snapd` source at +`$GOPATH/src/github.com/snapcore/snapd`. The source for any +dependent packages will also be available inside `$GOPATH`. + +### Dependencies handling + +Dependencies are handled via `govendor`. Get it via: + + go get -u github.com/kardianos/govendor + +After a fresh checkout, move to the snapd source directory: + + cd $GOPATH/src/github.com/snapcore/snapd + +And then, run: + + govendor sync + +You can use the script `get-deps.sh` to run the two previous steps. + +If a dependency need updating + + govendor fetch github.com/path/of/dependency + +### Building + +To build, once the sources are available and `GOPATH` is set, you can just run + + go build -o /tmp/snap github.com/snapcore/snapd/cmd/snap + +to get the `snap` binary in /tmp (or without -o to get it in the current +working directory). Alternatively: + + go install github.com/snapcore/snapd/cmd/snap/... + +to have it available in `$GOPATH/bin` + +Similarly, to build the `snapd` REST API daemon, you can run + + go build -o /tmp/snapd github.com/snapcore/snapd/cmd/snapd + +### Contributing + +Contributions are always welcome! Please make sure that you sign the +Canonical contributor licence agreement at +http://www.ubuntu.com/legal/contributors + +Snapd can be found on Github, so in order to fork the source and contribute, +go to https://github.com/snapcore/snapd. Check out [Github's help +pages](https://help.github.com/) to find out how to set up your local branch, +commit changes and create pull requests. + +We value good tests, so when you fix a bug or add a new feature we highly +encourage you to create a test in `$source_test.go`. See also the section +about Testing. + +### Testing + +To run the various tests that we have to ensure a high quality source just run: + + ./run-checks + +This will check if the source format is consistent, that it builds, all tests +work as expected and that "go vet" has nothing to complain. + +You can run individual test for a sub-package by changing into that directory and: + + go test -check.f $testname + +If a test hangs, you can enable verbose mode: + + go test -v -check.vv + +(or -check.v for less verbose output). + +There is more to read about the testing framework on the [website](https://labix.org/gocheck) + +### Running the spread tests + +To run the spread tests locally you need the latest version of spread +from https://github.com/snapcore/spread. It can be installed via: + + $ sudo apt install qemu-kvm autopkgtest + $ sudo snap install --devmode spread + +Then setup the environment via: + + $ mkdir -p .spread/qemu + $ cd .spread/qemu + # For xenial (same works for yakkety/zesty) + $ adt-buildvm-ubuntu-cloud -r xenial + $ mv adt-xenial-amd64-cloud.img ubuntu-16.04.img + # For trusty + $ adt-buildvm-ubuntu-cloud -r trusty --post-command='sudo apt-get install -y --install-recommends linux-generic-lts-xenial && update-grub' + $ mv adt-trusty-amd64-cloud.img ubuntu-14.04-64.img + + +And you can run the tests via: + + $ spread -v qemu: + +For quick reuse you can use: + + $ spread -reuse qemu: + +It will print how to reuse the systems. Make sure to use +`export REUSE_PROJECT=1` in your environment too. + + +### Testing snapd + +To test the `snapd` REST API daemon on a snappy system you need to +transfer it to the snappy system and then run: + + sudo systemctl stop snapd.service snapd.socket + sudo SNAPD_DEBUG=1 SNAPD_DEBUG_HTTP=3 ./snapd + +To debug interaction with the snap store, you can set `SNAP_DEBUG_HTTP`. +It is a bitfield: dump requests: 1, dump responses: 2, dump bodies: 4. + +(make hack: In case you get some security profiles errors when trying to install or refresh a snap, +maybe you need to replace system installed snap-seccomp with the one aligned to the snapd that +you are testing. To do this, simply backup /usr/lib/snapd/snap-seccomp and overwrite it with +the testing one. Don't forget to rollback to the original when finish testing) + +# Quick intro to hacking on snap-confine + +Hey, welcome to the nice, low-level world of snap-confine + +## Building the code locally + +To get started from a pristine tree you want to do this: + +``` +./mkversion.sh +cd cmd/ +autoreconf -i -f +./configure --prefix=/usr --libexecdir=/usr/lib/snapd --enable-nvidia-ubuntu +``` + +This will drop makefiles and let you build stuff. You may find the `make hack` +target, available in `cmd/snap-confine` handy, it installs the locally built +version on your system and reloads the apparmor profile. + +## Submitting patches + +Please run `make fmt` before sending your patches. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..f37c6d41 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ +Thanks for helping us make a better snapd! +Have you signed the [license agreement](https://www.ubuntu.com/legal/contributors) and read the [contribution guide](https://github.com/snapcore/snapd/blob/master/CONTRIBUTING.md)? diff --git a/README.md b/README.md new file mode 100644 index 00000000..0f2d375b --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +[![Build Status][travis-image]][travis-url] +[![Go Report Card][goreportcard-image]][goreportcard-url] +[![codecov][codecov-image]][codecov-url] + +## Snaps + +Package any app for every Linux desktop, server, cloud or device. + +Snaps are faster to install, easier to create, safer to run, and they update +automatically and transactionally so your app is always fresh and never +broken. You can bring your own build infrastructure or use ours. + +Head over to [snapcraft.io](https://snapcraft.io) to get started. + +## Development + +To get started with development off the snapd code itself, please check +out [HACKING.md](https://github.com/snapcore/snapd/blob/master/HACKING.md) +for in-depth details. + +## Reporting bugs + +If you have found an issue with the application, please [file a bug](https://bugs.launchpad.net/snappy/+filebug) on the [bugs list on Launchpad](https://bugs.launchpad.net/snappy/). + +## Get in touch + +We're friendly! Talk to us on +[IRC](https://webchat.freenode.net/?channels=snappy), +[Rocket Chat](https://rocket.ubuntu.com/channel/snappy), +or on [our forums](https://forum.snapcraft.io/). + +Get news and stay up to date on [Twitter](https://twitter.com/snapcraftio), +[Google+](https://plus.google.com/+SnapcraftIo) or +[Facebook](https://www.facebook.com/snapcraftio). + + + +[travis-image]: https://travis-ci.org/snapcore/snapd.svg?branch=master +[travis-url]: https://travis-ci.org/snapcore/snapd + +[goreportcard-image]: https://goreportcard.com/badge/github.com/snapcore/snapd +[goreportcard-url]: https://goreportcard.com/report/github.com/snapcore/snapd + +[coveralls-image]: https://coveralls.io/repos/snapcore/snapd/badge.svg?branch=master&service=github +[coveralls-url]: https://coveralls.io/github/snapcore/snapd?branch=master + +[codecov-url]: https://codecov.io/gh/snapcore/snapd +[codecov-image]: https://codecov.io/gh/snapcore/snapd/branch/master/graph/badge.svg diff --git a/arch/arch.go b/arch/arch.go new file mode 100644 index 00000000..e6978c05 --- /dev/null +++ b/arch/arch.go @@ -0,0 +1,154 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package arch + +import ( + "log" + "runtime" + "syscall" + + "github.com/snapcore/snapd/release" +) + +// ArchitectureType is the type for a supported snappy architecture +type ArchitectureType string + +// arch is global to allow tools like ubuntu-device-flash to +// change the architecture. This is important to e.g. install +// armhf snaps onto a armhf image that is generated on an amd64 +// machine +var arch = ArchitectureType(ubuntuArchFromGoArch(runtime.GOARCH)) + +// SetArchitecture allows overriding the auto detected Architecture +func SetArchitecture(newArch ArchitectureType) { + arch = newArch +} + +// FIXME: rename all Ubuntu*Architecture() to SnapdArchitecture() +// (or DpkgArchitecture) + +// UbuntuArchitecture returns the debian equivalent architecture for the +// currently running architecture. +// +// If the architecture does not map any debian architecture, the +// GOARCH is returned. +func UbuntuArchitecture() string { + return string(arch) +} + +// ubuntuArchFromGoArch maps a go architecture string to the coresponding +// Ubuntu architecture string. +// +// E.g. the go "386" architecture string maps to the ubuntu "i386" +// architecture. +func ubuntuArchFromGoArch(goarch string) string { + goArchMapping := map[string]string{ + // go ubuntu + "386": "i386", + "amd64": "amd64", + "arm": "armhf", + "arm64": "arm64", + "ppc64le": "ppc64el", + "s390x": "s390x", + "ppc": "powerpc", + // available in debian and other distros + "ppc64": "ppc64", + } + + // If we are running on an ARM platform we need to have a + // closer look if we are on armhf or armel. If we're not + // on a armv6 platform we can continue to use the Go + // arch mapping. The Go arch sadly doesn't map this out + // for us so we have to fallback to uname here. + if goarch == "arm" { + machineName := release.Machine() + if machineName == "armv6l" { + return "armel" + } + } + + ubuntuArch := goArchMapping[goarch] + if ubuntuArch == "" { + log.Panicf("unknown goarch %q", goarch) + } + + return ubuntuArch +} + +// UbuntuKernelArchitecture return the debian equivalent architecture +// for the current running kernel. This is usually the same as the +// UbuntuArchitecture - however there maybe cases that you run e.g. +// a snapd:i386 on an amd64 kernel. +func UbuntuKernelArchitecture() string { + var utsname syscall.Utsname + if err := syscall.Uname(&utsname); err != nil { + log.Panicf("cannot get kernel architecture: %v", err) + } + + // syscall.Utsname{} is using [65]int8 for all char[] inside it, + // this makes converting it so awkward. The alternative would be + // to use a unsafe.Pointer() to cast it to a [65]byte slice. + // see https://github.com/golang/go/issues/20753 + kernelArch := make([]byte, 0, len(utsname.Machine)) + for _, c := range utsname.Machine { + if c == 0 { + break + } + kernelArch = append(kernelArch, byte(c)) + } + + return ubuntuArchFromKernelArch(string(kernelArch)) +} + +// ubuntuArchFromkernelArch maps the kernel architecture as reported +// via uname() to the dpkg architecture +func ubuntuArchFromKernelArch(utsMachine string) string { + kernelArchMapping := map[string]string{ + // kernel ubuntu + "i686": "i386", + "x86_64": "amd64", + "armv7l": "armhf", + "aarch64": "arm64", + "ppc64le": "ppc64el", + "s390x": "s390x", + "ppc": "powerpc", + // available in debian and other distros + "ppc64": "ppc64", + } + + ubuntuArch := kernelArchMapping[utsMachine] + if ubuntuArch == "" { + log.Panicf("unknown kernel arch %q", utsMachine) + } + + return ubuntuArch +} + +// IsSupportedArchitecture returns true if the system architecture is in the +// list of architectures. +func IsSupportedArchitecture(architectures []string) bool { + for _, a := range architectures { + if a == "all" || a == string(arch) { + return true + } + } + + return false +} diff --git a/arch/arch_test.go b/arch/arch_test.go new file mode 100644 index 00000000..b08a1200 --- /dev/null +++ b/arch/arch_test.go @@ -0,0 +1,61 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package arch + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +var _ = Suite(&ArchTestSuite{}) + +type ArchTestSuite struct { +} + +func (ts *ArchTestSuite) TestUbuntuArchitecture(c *C) { + c.Check(ubuntuArchFromGoArch("386"), Equals, "i386") + c.Check(ubuntuArchFromGoArch("amd64"), Equals, "amd64") + c.Check(ubuntuArchFromGoArch("arm"), Equals, "armhf") + c.Check(ubuntuArchFromGoArch("arm64"), Equals, "arm64") + c.Check(ubuntuArchFromGoArch("ppc64le"), Equals, "ppc64el") + c.Check(ubuntuArchFromGoArch("ppc64"), Equals, "ppc64") + c.Check(ubuntuArchFromGoArch("s390x"), Equals, "s390x") +} + +func (ts *ArchTestSuite) TestSetArchitecture(c *C) { + SetArchitecture("armhf") + c.Assert(UbuntuArchitecture(), Equals, "armhf") +} + +func (ts *ArchTestSuite) TestSupportedArchitectures(c *C) { + arch = "armhf" + c.Check(IsSupportedArchitecture([]string{"all"}), Equals, true) + c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true) + c.Check(IsSupportedArchitecture([]string{"armhf"}), Equals, true) + c.Check(IsSupportedArchitecture([]string{"amd64", "powerpc"}), Equals, false) + + arch = "amd64" + c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true) + c.Check(IsSupportedArchitecture([]string{"powerpc"}), Equals, false) +} diff --git a/asserts/account.go b/asserts/account.go new file mode 100644 index 00000000..e9979ed0 --- /dev/null +++ b/asserts/account.go @@ -0,0 +1,106 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "regexp" + "time" +) + +var ( + accountValidationCertified = "certified" + + // account ids look like snap-ids or a nice identifier + validAccountID = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28})$") +) + +// Account holds an account assertion, which ties a name for an account +// to its identifier and provides the authority's confidence in the name's validity. +type Account struct { + assertionBase + certified bool + timestamp time.Time +} + +// AccountID returns the account-id of the account. +func (acc *Account) AccountID() string { + return acc.HeaderString("account-id") +} + +// Username returns the user name for the account. +func (acc *Account) Username() string { + return acc.HeaderString("username") +} + +// DisplayName returns the human-friendly name for the account. +func (acc *Account) DisplayName() string { + return acc.HeaderString("display-name") +} + +// IsCertified returns true if the authority has confidence in the account's name. +func (acc *Account) IsCertified() bool { + return acc.certified +} + +// Timestamp returns the time when the account was issued. +func (acc *Account) Timestamp() time.Time { + return acc.timestamp +} + +// Implement further consistency checks. +func (acc *Account) checkConsistency(db RODatabase, acck *AccountKey) error { + if !db.IsTrustedAccount(acc.AuthorityID()) { + return fmt.Errorf("account assertion for %q is not signed by a directly trusted authority: %s", acc.AccountID(), acc.AuthorityID()) + } + return nil +} + +// sanity +var _ consistencyChecker = (*Account)(nil) + +func assembleAccount(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "display-name") + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "validation") + if err != nil { + return nil, err + } + certified := assert.headers["validation"] == accountValidationCertified + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + _, err = checkOptionalString(assert.headers, "username") + if err != nil { + return nil, err + } + + return &Account{ + assertionBase: assert, + certified: certified, + timestamp: timestamp, + }, nil +} diff --git a/asserts/account_key.go b/asserts/account_key.go new file mode 100644 index 00000000..17ee949f --- /dev/null +++ b/asserts/account_key.go @@ -0,0 +1,288 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "regexp" + "time" +) + +var validAccountKeyName = regexp.MustCompile(`^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$`) + +// AccountKey holds an account-key assertion, asserting a public key +// belonging to the account. +type AccountKey struct { + assertionBase + since time.Time + until time.Time + pubKey PublicKey +} + +// AccountID returns the account-id of this account-key. +func (ak *AccountKey) AccountID() string { + return ak.HeaderString("account-id") +} + +// Name returns the name of the account key. +func (ak *AccountKey) Name() string { + return ak.HeaderString("name") +} + +func IsValidAccountKeyName(name string) bool { + return validAccountKeyName.MatchString(name) +} + +// Since returns the time when the account key starts being valid. +func (ak *AccountKey) Since() time.Time { + return ak.since +} + +// Until returns the time when the account key stops being valid. A zero time means the key is valid forever. +func (ak *AccountKey) Until() time.Time { + return ak.until +} + +// PublicKeyID returns the key id used for lookup of the account key. +func (ak *AccountKey) PublicKeyID() string { + return ak.pubKey.ID() +} + +// isKeyValidAt returns whether the account key is valid at 'when' time. +func (ak *AccountKey) isKeyValidAt(when time.Time) bool { + valid := when.After(ak.since) || when.Equal(ak.since) + if valid && !ak.until.IsZero() { + valid = when.Before(ak.until) + } + return valid +} + +// publicKey returns the underlying public key of the account key. +func (ak *AccountKey) publicKey() PublicKey { + return ak.pubKey +} + +func checkPublicKey(ab *assertionBase, keyIDName string) (PublicKey, error) { + pubKey, err := DecodePublicKey(ab.Body()) + if err != nil { + return nil, err + } + keyID, err := checkNotEmptyString(ab.headers, keyIDName) + if err != nil { + return nil, err + } + if keyID != pubKey.ID() { + return nil, fmt.Errorf("public key does not match provided key id") + } + return pubKey, nil +} + +// Implement further consistency checks. +func (ak *AccountKey) checkConsistency(db RODatabase, acck *AccountKey) error { + if !db.IsTrustedAccount(ak.AuthorityID()) { + return fmt.Errorf("account-key assertion for %q is not signed by a directly trusted authority: %s", ak.AccountID(), ak.AuthorityID()) + } + _, err := db.Find(AccountType, map[string]string{ + "account-id": ak.AccountID(), + }) + if IsNotFound(err) { + return fmt.Errorf("account-key assertion for %q does not have a matching account assertion", ak.AccountID()) + } + if err != nil { + return err + } + // XXX: Make this unconditional once account-key assertions are required to have a name. + if ak.Name() != "" { + // Check that we don't end up with multiple keys with + // different IDs but the same account-id and name. + // Note that this is a non-transactional check-then-add, so + // is not a hard guarantee. Backstores that can implement a + // unique constraint should do so. + assertions, err := db.FindMany(AccountKeyType, map[string]string{ + "account-id": ak.AccountID(), + "name": ak.Name(), + }) + if err != nil && !IsNotFound(err) { + return err + } + for _, assertion := range assertions { + existingAccKey := assertion.(*AccountKey) + if ak.PublicKeyID() != existingAccKey.PublicKeyID() { + return fmt.Errorf("account-key assertion for %q with ID %q has the same name %q as existing ID %q", ak.AccountID(), ak.PublicKeyID(), ak.Name(), existingAccKey.PublicKeyID()) + } + } + } + return nil +} + +// sanity +var _ consistencyChecker = (*AccountKey)(nil) + +// Prerequisites returns references to this account-key's prerequisite assertions. +func (ak *AccountKey) Prerequisites() []*Ref { + return []*Ref{ + {Type: AccountType, PrimaryKey: []string{ak.AccountID()}}, + } +} + +func assembleAccountKey(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "account-id") + if err != nil { + return nil, err + } + + // XXX: We should require name to be present after backfilling existing assertions. + _, ok := assert.headers["name"] + if ok { + _, err = checkStringMatches(assert.headers, "name", validAccountKeyName) + if err != nil { + return nil, err + } + } + + since, err := checkRFC3339Date(assert.headers, "since") + if err != nil { + return nil, err + } + + until, err := checkRFC3339DateWithDefault(assert.headers, "until", time.Time{}) + if err != nil { + return nil, err + } + if !until.IsZero() && until.Before(since) { + return nil, fmt.Errorf("'until' time cannot be before 'since' time") + } + + pubk, err := checkPublicKey(&assert, "public-key-sha3-384") + if err != nil { + return nil, err + } + + // ignore extra headers for future compatibility + return &AccountKey{ + assertionBase: assert, + since: since, + until: until, + pubKey: pubk, + }, nil +} + +// AccountKeyRequest holds an account-key-request assertion, which is a self-signed request to prove that the requester holds the private key and wishes to create an account-key assertion for it. +type AccountKeyRequest struct { + assertionBase + since time.Time + until time.Time + pubKey PublicKey +} + +// AccountID returns the account-id of this account-key-request. +func (akr *AccountKeyRequest) AccountID() string { + return akr.HeaderString("account-id") +} + +// Name returns the name of the account key. +func (akr *AccountKeyRequest) Name() string { + return akr.HeaderString("name") +} + +// Since returns the time when the requested account key starts being valid. +func (akr *AccountKeyRequest) Since() time.Time { + return akr.since +} + +// Until returns the time when the requested account key stops being valid. A zero time means the key is valid forever. +func (akr *AccountKeyRequest) Until() time.Time { + return akr.until +} + +// PublicKeyID returns the underlying public key ID of the requested account key. +func (akr *AccountKeyRequest) PublicKeyID() string { + return akr.pubKey.ID() +} + +// signKey returns the underlying public key of the requested account key. +func (akr *AccountKeyRequest) signKey() PublicKey { + return akr.pubKey +} + +// Implement further consistency checks. +func (akr *AccountKeyRequest) checkConsistency(db RODatabase, acck *AccountKey) error { + _, err := db.Find(AccountType, map[string]string{ + "account-id": akr.AccountID(), + }) + if IsNotFound(err) { + return fmt.Errorf("account-key-request assertion for %q does not have a matching account assertion", akr.AccountID()) + } + if err != nil { + return err + } + return nil +} + +// sanity +var ( + _ consistencyChecker = (*AccountKeyRequest)(nil) + _ customSigner = (*AccountKeyRequest)(nil) +) + +// Prerequisites returns references to this account-key-request's prerequisite assertions. +func (akr *AccountKeyRequest) Prerequisites() []*Ref { + return []*Ref{ + {Type: AccountType, PrimaryKey: []string{akr.AccountID()}}, + } +} + +func assembleAccountKeyRequest(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "account-id") + if err != nil { + return nil, err + } + + _, err = checkStringMatches(assert.headers, "name", validAccountKeyName) + if err != nil { + return nil, err + } + + since, err := checkRFC3339Date(assert.headers, "since") + if err != nil { + return nil, err + } + + until, err := checkRFC3339DateWithDefault(assert.headers, "until", time.Time{}) + if err != nil { + return nil, err + } + if !until.IsZero() && until.Before(since) { + return nil, fmt.Errorf("'until' time cannot be before 'since' time") + } + + pubk, err := checkPublicKey(&assert, "public-key-sha3-384") + if err != nil { + return nil, err + } + + // ignore extra headers for future compatibility + return &AccountKeyRequest{ + assertionBase: assert, + since: since, + until: until, + pubKey: pubk, + }, nil +} diff --git a/asserts/account_key_test.go b/asserts/account_key_test.go new file mode 100644 index 00000000..d1fd153b --- /dev/null +++ b/asserts/account_key_test.go @@ -0,0 +1,809 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "encoding/base64" + "fmt" + "path/filepath" + "strings" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +type accountKeySuite struct { + privKey asserts.PrivateKey + pubKeyBody string + keyID string + since, until time.Time + sinceLine, untilLine string +} + +var _ = Suite(&accountKeySuite{}) + +func (aks *accountKeySuite) SetUpSuite(c *C) { + cfg1 := &asserts.DatabaseConfig{} + accDb, err := asserts.OpenDatabase(cfg1) + c.Assert(err, IsNil) + aks.privKey = testPrivKey1 + err = accDb.ImportKey(aks.privKey) + c.Assert(err, IsNil) + aks.keyID = aks.privKey.PublicKey().ID() + + pubKey, err := accDb.PublicKey(aks.keyID) + c.Assert(err, IsNil) + pubKeyEncoded, err := asserts.EncodePublicKey(pubKey) + c.Assert(err, IsNil) + aks.pubKeyBody = string(pubKeyEncoded) + + aks.since, err = time.Parse(time.RFC822, "16 Nov 15 15:04 UTC") + c.Assert(err, IsNil) + aks.until = aks.since.AddDate(1, 0, 0) + aks.sinceLine = "since: " + aks.since.Format(time.RFC3339) + "\n" + aks.untilLine = "until: " + aks.until.Format(time.RFC3339) + "\n" +} + +func (aks *accountKeySuite) TestDecodeOK(c *C) { + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.AccountKeyType) + accKey := a.(*asserts.AccountKey) + c.Check(accKey.AccountID(), Equals, "acc-id1") + c.Check(accKey.Name(), Equals, "default") + c.Check(accKey.PublicKeyID(), Equals, aks.keyID) + c.Check(accKey.Since(), Equals, aks.since) +} + +func (aks *accountKeySuite) TestDecodeNoName(c *C) { + // XXX: remove this test once name is mandatory + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.AccountKeyType) + accKey := a.(*asserts.AccountKey) + c.Check(accKey.AccountID(), Equals, "acc-id1") + c.Check(accKey.Name(), Equals, "") + c.Check(accKey.PublicKeyID(), Equals, aks.keyID) + c.Check(accKey.Since(), Equals, aks.since) +} + +func (aks *accountKeySuite) TestUntil(c *C) { + + untilSinceLine := "until: " + aks.since.Format(time.RFC3339) + "\n" + + tests := []struct { + untilLine string + until time.Time + }{ + {"", time.Time{}}, // zero time default + {aks.untilLine, aks.until}, // in the future + {untilSinceLine, aks.since}, // same as since + } + + for _, test := range tests { + c.Log(test) + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + test.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "openpgp c2ln" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + accKey := a.(*asserts.AccountKey) + c.Check(accKey.Until(), Equals, test.until) + } +} + +const ( + accKeyErrPrefix = "assertion account-key: " + accKeyReqErrPrefix = "assertion account-key-request: " +) + +func (aks *accountKeySuite) TestDecodeInvalidHeaders(c *C) { + + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + + untilPast := aks.since.AddDate(-1, 0, 0) + untilPastLine := "until: " + untilPast.Format(time.RFC3339) + "\n" + + invalidHeaderTests := []struct{ original, invalid, expectedErr string }{ + {"account-id: acc-id1\n", "", `"account-id" header is mandatory`}, + {"account-id: acc-id1\n", "account-id: \n", `"account-id" header should not be empty`}, + // XXX: enable this once name is mandatory + // {"name: default\n", "", `"name" header is mandatory`}, + {"name: default\n", "name: \n", `"name" header should not be empty`}, + {"name: default\n", "name: a b\n", `"name" header contains invalid characters: "a b"`}, + {"name: default\n", "name: -default\n", `"name" header contains invalid characters: "-default"`}, + {"name: default\n", "name: foo:bar\n", `"name" header contains invalid characters: "foo:bar"`}, + {"name: default\n", "name: a--b\n", `"name" header contains invalid characters: "a--b"`}, + {"name: default\n", "name: 42\n", `"name" header contains invalid characters: "42"`}, + {"public-key-sha3-384: " + aks.keyID + "\n", "", `"public-key-sha3-384" header is mandatory`}, + {"public-key-sha3-384: " + aks.keyID + "\n", "public-key-sha3-384: \n", `"public-key-sha3-384" header should not be empty`}, + {aks.sinceLine, "", `"since" header is mandatory`}, + {aks.sinceLine, "since: \n", `"since" header should not be empty`}, + {aks.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`}, + {aks.sinceLine, "since: \n", `"since" header should not be empty`}, + {aks.untilLine, "until: \n", `"until" header is not a RFC3339 date: .*`}, + {aks.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`}, + {aks.untilLine, untilPastLine, `'until' time cannot be before 'since' time`}, + } + + for _, test := range invalidHeaderTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyErrPrefix+test.expectedErr) + } +} + +func (aks *accountKeySuite) TestDecodeInvalidPublicKey(c *C) { + headers := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + raw, err := base64.StdEncoding.DecodeString(aks.pubKeyBody) + c.Assert(err, IsNil) + spurious := base64.StdEncoding.EncodeToString(append(raw, "gorp"...)) + + invalidPublicKeyTests := []struct{ body, expectedErr string }{ + {"", "cannot decode public key: no data"}, + {"==", "cannot decode public key: .*"}, + {"stuff", "cannot decode public key: .*"}, + {"AnNpZw==", "unsupported public key format version: 2"}, + {"AUJST0tFTg==", "cannot decode public key: .*"}, + {spurious, "public key has spurious trailing data"}, + } + + for _, test := range invalidPublicKeyTests { + invalid := headers + + fmt.Sprintf("body-length: %v", len(test.body)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + test.body + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyErrPrefix+test.expectedErr) + } +} + +func (aks *accountKeySuite) TestDecodeKeyIDMismatch(c *C) { + invalid := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: aa\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyErrPrefix+"public key does not match provided key id") +} + +func (aks *accountKeySuite) openDB(c *C) *asserts.Database { + trustedKey := testPrivKey0 + + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + cfg := &asserts.DatabaseConfig{ + Backstore: bs, + Trusted: []asserts.Assertion{ + asserts.BootstrapAccountForTest("canonical"), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey()), + }, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + return db +} + +func (aks *accountKeySuite) prereqAccount(c *C, db *asserts.Database) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "display-name": "Acct1", + "account-id": "acc-id1", + "username": "acc-id1", + "validation": "unproven", + "timestamp": aks.since.Format(time.RFC3339), + } + acct1, err := asserts.AssembleAndSignInTest(asserts.AccountType, headers, nil, trustedKey) + c.Assert(err, IsNil) + + // prereq + db.Add(acct1) +} + +func (aks *accountKeySuite) TestAccountKeyCheck(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + + aks.prereqAccount(c, db) + + err = db.Check(accKey) + c.Assert(err, IsNil) +} + +func (aks *accountKeySuite) TestAccountKeyCheckNoAccount(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + + err = db.Check(accKey) + c.Assert(err, ErrorMatches, `account-key assertion for "acc-id1" does not have a matching account assertion`) +} + +func (aks *accountKeySuite) TestAccountKeyCheckUntrustedAuthority(c *C) { + trustedKey := testPrivKey0 + + db := aks.openDB(c) + storeDB := assertstest.NewSigningDB("canonical", trustedKey) + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := otherDB.Sign(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), "") + c.Assert(err, IsNil) + + err = db.Check(accKey) + c.Assert(err, ErrorMatches, `account-key assertion for "acc-id1" is not signed by a directly trusted authority:.*`) +} + +func (aks *accountKeySuite) TestAccountKeyCheckSameNameAndNewRevision(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + headers["revision"] = "1" + newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + err = db.Check(newAccKey) + c.Assert(err, IsNil) +} + +func (aks *accountKeySuite) TestAccountKeyCheckSameAccountAndDifferentName(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + newPrivKey, _ := assertstest.GenerateKey(752) + err = db.ImportKey(newPrivKey) + c.Assert(err, IsNil) + newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID()) + c.Assert(err, IsNil) + newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey) + c.Assert(err, IsNil) + + headers["name"] = "another" + headers["public-key-sha3-384"] = newPubKey.ID() + newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey) + c.Assert(err, IsNil) + + err = db.Check(newAccKey) + c.Assert(err, IsNil) +} + +func (aks *accountKeySuite) TestAccountKeyCheckSameNameAndDifferentAccount(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + err = db.ImportKey(trustedKey) + c.Assert(err, IsNil) + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + newPrivKey, _ := assertstest.GenerateKey(752) + err = db.ImportKey(newPrivKey) + c.Assert(err, IsNil) + newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID()) + c.Assert(err, IsNil) + newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey) + c.Assert(err, IsNil) + + acct2 := assertstest.NewAccount(db, "acc-id2", map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id2", + }, trustedKey.PublicKey().ID()) + db.Add(acct2) + + headers["account-id"] = "acc-id2" + headers["public-key-sha3-384"] = newPubKey.ID() + headers["revision"] = "1" + newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey) + c.Assert(err, IsNil) + + err = db.Check(newAccKey) + c.Assert(err, IsNil) +} + +func (aks *accountKeySuite) TestAccountKeyCheckNameClash(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + newPrivKey, _ := assertstest.GenerateKey(752) + err = db.ImportKey(newPrivKey) + c.Assert(err, IsNil) + newPubKey, err := db.PublicKey(newPrivKey.PublicKey().ID()) + c.Assert(err, IsNil) + newPubKeyEncoded, err := asserts.EncodePublicKey(newPubKey) + c.Assert(err, IsNil) + + headers["public-key-sha3-384"] = newPubKey.ID() + headers["revision"] = "1" + newAccKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, newPubKeyEncoded, trustedKey) + c.Assert(err, IsNil) + + err = db.Check(newAccKey) + c.Assert(err, ErrorMatches, fmt.Sprintf(`account-key assertion for "acc-id1" with ID %q has the same name "default" as existing ID %q`, newPubKey.ID(), aks.keyID)) +} + +func (aks *accountKeySuite) TestAccountKeyAddAndFind(c *C) { + trustedKey := testPrivKey0 + + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + "until": aks.until.Format(time.RFC3339), + } + accKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, []byte(aks.pubKeyBody), trustedKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + + aks.prereqAccount(c, db) + + err = db.Add(accKey) + c.Assert(err, IsNil) + + found, err := db.Find(asserts.AccountKeyType, map[string]string{ + "account-id": "acc-id1", + "public-key-sha3-384": aks.keyID, + }) + c.Assert(err, IsNil) + c.Assert(found, NotNil) + c.Check(found.Body(), DeepEquals, []byte(aks.pubKeyBody)) +} + +func (aks *accountKeySuite) TestPublicKeyIsValidAt(c *C) { + // With since and until, i.e. signing account-key expires. + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + accKey := a.(*asserts.AccountKey) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, true) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, true) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, -1, 0)), Equals, true) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, 1, 0)), Equals, false) + + // With no until, i.e. signing account-key never expires. + encoded = "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "openpgp c2ln" + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + accKey = a.(*asserts.AccountKey) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, true) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, true) + + // With since == until, i.e. signing account-key has been revoked. + encoded = "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + "until: " + aks.since.Format(time.RFC3339) + "\n" + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "openpgp c2ln" + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + accKey = a.(*asserts.AccountKey) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, -1)), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.since.AddDate(0, 0, 1)), Equals, false) + + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, -1, 0)), Equals, false) + c.Check(asserts.AccountKeyIsKeyValidAt(accKey, aks.until.AddDate(0, 1, 0)), Equals, false) +} + +func (aks *accountKeySuite) TestPrerequisites(c *C) { + encoded := "type: account-key\n" + + "authority-id: canonical\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 1) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"acc-id1"}, + }) +} + +func (aks *accountKeySuite) TestAccountKeyRequestHappy(c *C) { + akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, + map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + }, []byte(aks.pubKeyBody), aks.privKey) + c.Assert(err, IsNil) + + // roundtrip + a, err := asserts.Decode(asserts.Encode(akr)) + c.Assert(err, IsNil) + + akr2, ok := a.(*asserts.AccountKeyRequest) + c.Assert(ok, Equals, true) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Check(akr2) + c.Check(err, IsNil) + + c.Check(akr2.AccountID(), Equals, "acc-id1") + c.Check(akr2.Name(), Equals, "default") + c.Check(akr2.PublicKeyID(), Equals, aks.keyID) + c.Check(akr2.Since(), Equals, aks.since) +} + +func (aks *accountKeySuite) TestAccountKeyRequestUntil(c *C) { + db := aks.openDB(c) + aks.prereqAccount(c, db) + + tests := []struct { + untilHeader string + until time.Time + }{ + {"", time.Time{}}, // zero time default + {aks.until.Format(time.RFC3339), aks.until}, // in the future + {aks.since.Format(time.RFC3339), aks.since}, // same as since + } + + for _, test := range tests { + c.Log(test) + headers := map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + } + if test.untilHeader != "" { + headers["until"] = test.untilHeader + } + akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, []byte(aks.pubKeyBody), aks.privKey) + c.Assert(err, IsNil) + a, err := asserts.Decode(asserts.Encode(akr)) + c.Assert(err, IsNil) + akr2 := a.(*asserts.AccountKeyRequest) + c.Check(akr2.Until(), Equals, test.until) + err = db.Check(akr2) + c.Check(err, IsNil) + } +} + +func (aks *accountKeySuite) TestAccountKeyRequestAddAndFind(c *C) { + akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, + map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + }, []byte(aks.pubKeyBody), aks.privKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + aks.prereqAccount(c, db) + + err = db.Add(akr) + c.Assert(err, IsNil) + + found, err := db.Find(asserts.AccountKeyRequestType, map[string]string{ + "account-id": "acc-id1", + "public-key-sha3-384": aks.keyID, + }) + c.Assert(err, IsNil) + c.Assert(found, NotNil) + c.Check(found.Body(), DeepEquals, []byte(aks.pubKeyBody)) +} + +func (aks *accountKeySuite) TestAccountKeyRequestDecodeInvalid(c *C) { + encoded := "type: account-key-request\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + + untilPast := aks.since.AddDate(-1, 0, 0) + untilPastLine := "until: " + untilPast.Format(time.RFC3339) + "\n" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"account-id: acc-id1\n", "", `"account-id" header is mandatory`}, + {"account-id: acc-id1\n", "account-id: \n", `"account-id" header should not be empty`}, + {"name: default\n", "", `"name" header is mandatory`}, + {"name: default\n", "name: \n", `"name" header should not be empty`}, + {"name: default\n", "name: a b\n", `"name" header contains invalid characters: "a b"`}, + {"name: default\n", "name: -default\n", `"name" header contains invalid characters: "-default"`}, + {"name: default\n", "name: foo:bar\n", `"name" header contains invalid characters: "foo:bar"`}, + {"public-key-sha3-384: " + aks.keyID + "\n", "", `"public-key-sha3-384" header is mandatory`}, + {"public-key-sha3-384: " + aks.keyID + "\n", "public-key-sha3-384: \n", `"public-key-sha3-384" header should not be empty`}, + {aks.sinceLine, "", `"since" header is mandatory`}, + {aks.sinceLine, "since: \n", `"since" header should not be empty`}, + {aks.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`}, + {aks.sinceLine, "since: \n", `"since" header should not be empty`}, + {aks.untilLine, "until: \n", `"until" header is not a RFC3339 date: .*`}, + {aks.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`}, + {aks.untilLine, untilPastLine, `'until' time cannot be before 'since' time`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyReqErrPrefix+test.expectedErr) + } +} + +func (aks *accountKeySuite) TestAccountKeyRequestDecodeInvalidPublicKey(c *C) { + headers := "type: account-key-request\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: " + aks.keyID + "\n" + + aks.sinceLine + + aks.untilLine + + raw, err := base64.StdEncoding.DecodeString(aks.pubKeyBody) + c.Assert(err, IsNil) + spurious := base64.StdEncoding.EncodeToString(append(raw, "gorp"...)) + + invalidPublicKeyTests := []struct{ body, expectedErr string }{ + {"", "cannot decode public key: no data"}, + {"==", "cannot decode public key: .*"}, + {"stuff", "cannot decode public key: .*"}, + {"AnNpZw==", "unsupported public key format version: 2"}, + {"AUJST0tFTg==", "cannot decode public key: .*"}, + {spurious, "public key has spurious trailing data"}, + } + + for _, test := range invalidPublicKeyTests { + invalid := headers + + fmt.Sprintf("body-length: %v", len(test.body)) + "\n" + + "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" + + test.body + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accKeyReqErrPrefix+test.expectedErr) + } +} + +func (aks *accountKeySuite) TestAccountKeyRequestDecodeKeyIDMismatch(c *C) { + invalid := "type: account-key-request\n" + + "account-id: acc-id1\n" + + "name: default\n" + + "public-key-sha3-384: aa\n" + + aks.sinceLine + + aks.untilLine + + fmt.Sprintf("body-length: %v", len(aks.pubKeyBody)) + "\n" + + "sign-key-sha3-384: " + aks.privKey.PublicKey().ID() + "\n\n" + + aks.pubKeyBody + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, "assertion account-key-request: public key does not match provided key id") +} + +func (aks *accountKeySuite) TestAccountKeyRequestNoAccount(c *C) { + headers := map[string]interface{}{ + "account-id": "acc-id1", + "name": "default", + "public-key-sha3-384": aks.keyID, + "since": aks.since.Format(time.RFC3339), + } + akr, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, []byte(aks.pubKeyBody), aks.privKey) + c.Assert(err, IsNil) + + db := aks.openDB(c) + + err = db.Check(akr) + c.Assert(err, ErrorMatches, `account-key-request assertion for "acc-id1" does not have a matching account assertion`) +} diff --git a/asserts/account_test.go b/asserts/account_test.go new file mode 100644 index 00000000..2ebbc46d --- /dev/null +++ b/asserts/account_test.go @@ -0,0 +1,169 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "strings" + "time" + + "github.com/snapcore/snapd/asserts" + . "gopkg.in/check.v1" +) + +var ( + _ = Suite(&accountSuite{}) +) + +type accountSuite struct { + ts time.Time + tsLine string +} + +func (s *accountSuite) SetUpSuite(c *C) { + s.ts = time.Now().Truncate(time.Second).UTC() + s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n" +} + +const accountExample = "type: account\n" + + "authority-id: canonical\n" + + "account-id: abc-123\n" + + "display-name: Nice User\n" + + "username: nice\n" + + "validation: certified\n" + + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + +func (s *accountSuite) TestDecodeOK(c *C) { + encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.AccountType) + account := a.(*asserts.Account) + c.Check(account.AuthorityID(), Equals, "canonical") + c.Check(account.Timestamp(), Equals, s.ts) + c.Check(account.AccountID(), Equals, "abc-123") + c.Check(account.DisplayName(), Equals, "Nice User") + c.Check(account.Username(), Equals, "nice") + c.Check(account.IsCertified(), Equals, true) +} + +func (s *accountSuite) TestOptional(c *C) { + encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + + tests := []struct{ original, replacement string }{ + {"username: nice\n", ""}, + {"username: nice\n", "username: \n"}, + } + + for _, test := range tests { + valid := strings.Replace(encoded, test.original, test.replacement, 1) + _, err := asserts.Decode([]byte(valid)) + c.Check(err, IsNil) + } +} + +func (s *accountSuite) TestIsCertified(c *C) { + tests := []struct { + value string + isCertified bool + }{ + {"certified", true}, + {"unproven", false}, + {"nonsense", false}, + } + + template := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + for _, test := range tests { + encoded := strings.Replace( + template, + "validation: certified\n", + fmt.Sprintf("validation: %s\n", test.value), + 1, + ) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + account := assert.(*asserts.Account) + c.Check(account.IsCertified(), Equals, test.isCertified) + } +} + +const ( + accountErrPrefix = "assertion account: " +) + +func (s *accountSuite) TestDecodeInvalid(c *C) { + encoded := strings.Replace(accountExample, "TSLINE", s.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"account-id: abc-123\n", "", `"account-id" header is mandatory`}, + {"account-id: abc-123\n", "account-id: \n", `"account-id" header should not be empty`}, + {"display-name: Nice User\n", "", `"display-name" header is mandatory`}, + {"display-name: Nice User\n", "display-name: \n", `"display-name" header should not be empty`}, + {"username: nice\n", "username:\n - foo\n - bar\n", `"username" header must be a string`}, + {"validation: certified\n", "", `"validation" header is mandatory`}, + {"validation: certified\n", "validation: \n", `"validation" header should not be empty`}, + {s.tsLine, "", `"timestamp" header is mandatory`}, + {s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, accountErrPrefix+test.expectedErr) + } +} + +func (s *accountSuite) TestCheckInconsistentTimestamp(c *C) { + ex, err := asserts.Decode([]byte(strings.Replace(accountExample, "TSLINE", s.tsLine, 1))) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + + headers := ex.Headers() + headers["timestamp"] = "2011-01-01T14:00:00Z" + account, err := storeDB.Sign(asserts.AccountType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(account) + c.Assert(err, ErrorMatches, `account assertion timestamp outside of signing key validity \(key valid since.*\)`) +} + +func (s *accountSuite) TestCheckUntrustedAuthority(c *C) { + ex, err := asserts.Decode([]byte(strings.Replace(accountExample, "TSLINE", s.tsLine, 1))) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := ex.Headers() + // default to signing db's authority + delete(headers, "authority-id") + headers["timestamp"] = time.Now().Format(time.RFC3339) + account, err := otherDB.Sign(asserts.AccountType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(account) + c.Assert(err, ErrorMatches, `account assertion for "abc-123" is not signed by a directly trusted authority:.*`) +} diff --git a/asserts/asserts.go b/asserts/asserts.go new file mode 100644 index 00000000..9ae1ff25 --- /dev/null +++ b/asserts/asserts.go @@ -0,0 +1,1017 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "bufio" + "bytes" + "crypto" + "fmt" + "io" + "sort" + "strconv" + "strings" + "unicode/utf8" +) + +type typeFlags int + +const ( + noAuthority typeFlags = iota + 1 +) + +// AssertionType describes a known assertion type with its name and metadata. +type AssertionType struct { + // Name of the type. + Name string + // PrimaryKey holds the names of the headers that constitute the + // unique primary key for this assertion type. + PrimaryKey []string + + assembler func(assert assertionBase) (Assertion, error) + flags typeFlags +} + +// MaxSupportedFormat returns the maximum supported format iteration for the type. +func (at *AssertionType) MaxSupportedFormat() int { + return maxSupportedFormat[at.Name] +} + +// Understood assertion types. +var ( + AccountType = &AssertionType{"account", []string{"account-id"}, assembleAccount, 0} + AccountKeyType = &AssertionType{"account-key", []string{"public-key-sha3-384"}, assembleAccountKey, 0} + RepairType = &AssertionType{"repair", []string{"brand-id", "repair-id"}, assembleRepair, 0} + ModelType = &AssertionType{"model", []string{"series", "brand-id", "model"}, assembleModel, 0} + SerialType = &AssertionType{"serial", []string{"brand-id", "model", "serial"}, assembleSerial, 0} + BaseDeclarationType = &AssertionType{"base-declaration", []string{"series"}, assembleBaseDeclaration, 0} + SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, assembleSnapDeclaration, 0} + SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, assembleSnapBuild, 0} + SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384"}, assembleSnapRevision, 0} + SnapDeveloperType = &AssertionType{"snap-developer", []string{"snap-id", "publisher-id"}, assembleSnapDeveloper, 0} + SystemUserType = &AssertionType{"system-user", []string{"brand-id", "email"}, assembleSystemUser, 0} + ValidationType = &AssertionType{"validation", []string{"series", "snap-id", "approved-snap-id", "approved-snap-revision"}, assembleValidation, 0} + StoreType = &AssertionType{"store", []string{"store"}, assembleStore, 0} + +// ... +) + +// Assertion types without a definite authority set (on the wire and/or self-signed). +var ( + DeviceSessionRequestType = &AssertionType{"device-session-request", []string{"brand-id", "model", "serial"}, assembleDeviceSessionRequest, noAuthority} + SerialRequestType = &AssertionType{"serial-request", nil, assembleSerialRequest, noAuthority} + AccountKeyRequestType = &AssertionType{"account-key-request", []string{"public-key-sha3-384"}, assembleAccountKeyRequest, noAuthority} +) + +var typeRegistry = map[string]*AssertionType{ + AccountType.Name: AccountType, + AccountKeyType.Name: AccountKeyType, + ModelType.Name: ModelType, + SerialType.Name: SerialType, + BaseDeclarationType.Name: BaseDeclarationType, + SnapDeclarationType.Name: SnapDeclarationType, + SnapBuildType.Name: SnapBuildType, + SnapRevisionType.Name: SnapRevisionType, + SnapDeveloperType.Name: SnapDeveloperType, + SystemUserType.Name: SystemUserType, + ValidationType.Name: ValidationType, + RepairType.Name: RepairType, + StoreType.Name: StoreType, + // no authority + DeviceSessionRequestType.Name: DeviceSessionRequestType, + SerialRequestType.Name: SerialRequestType, + AccountKeyRequestType.Name: AccountKeyRequestType, +} + +// Type returns the AssertionType with name or nil +func Type(name string) *AssertionType { + return typeRegistry[name] +} + +// TypeNames returns a sorted list of known assertion type names. +func TypeNames() []string { + names := make([]string, 0, len(typeRegistry)) + for k := range typeRegistry { + names = append(names, k) + } + + sort.Strings(names) + + return names +} + +var maxSupportedFormat = map[string]int{} + +func init() { + // register maxSupportedFormats while breaking initialisation loop + + // 1: plugs and slots + // 2: support for $SLOT()/$PLUG()/$MISSING + maxSupportedFormat[SnapDeclarationType.Name] = 2 +} + +func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) { + prev := maxSupportedFormat[assertType.Name] + maxSupportedFormat[assertType.Name] = maxFormat + return func() { + maxSupportedFormat[assertType.Name] = prev + } +} + +var formatAnalyzer = map[*AssertionType]func(headers map[string]interface{}, body []byte) (formatnum int, err error){ + SnapDeclarationType: snapDeclarationFormatAnalyze, +} + +// SuggestFormat returns a minimum format that supports the features that would be used by an assertion with the given components. +func SuggestFormat(assertType *AssertionType, headers map[string]interface{}, body []byte) (formatnum int, err error) { + analyzer := formatAnalyzer[assertType] + if analyzer == nil { + // no analyzer, format 0 is all there is + return 0, nil + } + formatnum, err = analyzer(headers, body) + if err != nil { + return 0, fmt.Errorf("assertion %s: %v", assertType.Name, err) + } + return formatnum, nil +} + +// HeadersFromPrimaryKey constructs a headers mapping from the +// primaryKey values and the assertion type, it errors if primaryKey +// has the wrong length. +func HeadersFromPrimaryKey(assertType *AssertionType, primaryKey []string) (headers map[string]string, err error) { + if len(primaryKey) != len(assertType.PrimaryKey) { + return nil, fmt.Errorf("primary key has wrong length for %q assertion", assertType.Name) + } + headers = make(map[string]string, len(assertType.PrimaryKey)) + for i, name := range assertType.PrimaryKey { + keyVal := primaryKey[i] + if keyVal == "" { + return nil, fmt.Errorf("primary key %q header cannot be empty", name) + } + headers[name] = keyVal + } + return headers, nil +} + +// PrimaryKeyFromHeaders extracts the tuple of values from headers +// corresponding to a primary key under the assertion type, it errors +// if there are missing primary key headers. +func PrimaryKeyFromHeaders(assertType *AssertionType, headers map[string]string) (primaryKey []string, err error) { + primaryKey = make([]string, len(assertType.PrimaryKey)) + for i, k := range assertType.PrimaryKey { + keyVal := headers[k] + if keyVal == "" { + return nil, fmt.Errorf("must provide primary key: %v", k) + } + primaryKey[i] = keyVal + } + return primaryKey, nil +} + +// Ref expresses a reference to an assertion. +type Ref struct { + Type *AssertionType + PrimaryKey []string +} + +func (ref *Ref) String() string { + pkStr := "-" + n := len(ref.Type.PrimaryKey) + if n != len(ref.PrimaryKey) { + pkStr = "???" + } else if n > 0 { + pkStr = ref.PrimaryKey[n-1] + if n > 1 { + sfx := []string{pkStr + ";"} + for i, k := range ref.Type.PrimaryKey[:n-1] { + sfx = append(sfx, fmt.Sprintf("%s:%s", k, ref.PrimaryKey[i])) + } + pkStr = strings.Join(sfx, " ") + } + } + return fmt.Sprintf("%s (%s)", ref.Type.Name, pkStr) +} + +// Unique returns a unique string representing the reference that can be used as a key in maps. +func (ref *Ref) Unique() string { + return fmt.Sprintf("%s/%s", ref.Type.Name, strings.Join(ref.PrimaryKey, "/")) +} + +// Resolve resolves the reference using the given find function. +func (ref *Ref) Resolve(find func(assertType *AssertionType, headers map[string]string) (Assertion, error)) (Assertion, error) { + headers, err := HeadersFromPrimaryKey(ref.Type, ref.PrimaryKey) + if err != nil { + return nil, fmt.Errorf("%q assertion reference primary key has the wrong length (expected %v): %v", ref.Type.Name, ref.Type.PrimaryKey, ref.PrimaryKey) + } + return find(ref.Type, headers) +} + +// Assertion represents an assertion through its general elements. +type Assertion interface { + // Type returns the type of this assertion + Type() *AssertionType + // Format returns the format iteration of this assertion + Format() int + // SupportedFormat returns whether the assertion uses a supported + // format iteration. If false the assertion might have been only + // partially parsed. + SupportedFormat() bool + // Revision returns the revision of this assertion + Revision() int + // AuthorityID returns the authority that signed this assertion + AuthorityID() string + + // Header retrieves the header with name + Header(name string) interface{} + + // Headers returns the complete headers + Headers() map[string]interface{} + + // HeaderString retrieves the string value of header with name or "" + HeaderString(name string) string + + // Body returns the body of this assertion + Body() []byte + + // Signature returns the signed content and its unprocessed signature + Signature() (content, signature []byte) + + // SignKeyID returns the key id for the key that signed this assertion. + SignKeyID() string + + // Prerequisites returns references to the prerequisite assertions for the validity of this one. + Prerequisites() []*Ref + + // Ref returns a reference representing this assertion. + Ref() *Ref +} + +// customSigner represents an assertion with special arrangements for its signing key (e.g. self-signed), rather than the usual case where an assertion is signed by its authority. +type customSigner interface { + // signKey returns the public key material for the key that signed this assertion. See also SignKeyID. + signKey() PublicKey +} + +// MediaType is the media type for encoded assertions on the wire. +const MediaType = "application/x.ubuntu.assertion" + +// assertionBase is the concrete base to hold representation data for actual assertions. +type assertionBase struct { + headers map[string]interface{} + body []byte + // parsed format iteration + format int + // parsed revision + revision int + // preserved content + content []byte + // unprocessed signature + signature []byte +} + +// HeaderString retrieves the string value of header with name or "" +func (ab *assertionBase) HeaderString(name string) string { + s, _ := ab.headers[name].(string) + return s +} + +// Type returns the assertion type. +func (ab *assertionBase) Type() *AssertionType { + return Type(ab.HeaderString("type")) +} + +// Format returns the assertion format iteration. +func (ab *assertionBase) Format() int { + return ab.format +} + +// SupportedFormat returns whether the assertion uses a supported +// format iteration. If false the assertion might have been only +// partially parsed. +func (ab *assertionBase) SupportedFormat() bool { + return ab.format <= maxSupportedFormat[ab.HeaderString("type")] +} + +// Revision returns the assertion revision. +func (ab *assertionBase) Revision() int { + return ab.revision +} + +// AuthorityID returns the authority-id a.k.a the signer id of the assertion. +func (ab *assertionBase) AuthorityID() string { + return ab.HeaderString("authority-id") +} + +// Header returns the value of an header by name. +func (ab *assertionBase) Header(name string) interface{} { + v := ab.headers[name] + if v == nil { + return nil + } + return copyHeader(v) +} + +// Headers returns the complete headers. +func (ab *assertionBase) Headers() map[string]interface{} { + return copyHeaders(ab.headers) +} + +// Body returns the body of the assertion. +func (ab *assertionBase) Body() []byte { + return ab.body +} + +// Signature returns the signed content and its unprocessed signature. +func (ab *assertionBase) Signature() (content, signature []byte) { + return ab.content, ab.signature +} + +// SignKeyID returns the key id for the key that signed this assertion. +func (ab *assertionBase) SignKeyID() string { + return ab.HeaderString("sign-key-sha3-384") +} + +// Prerequisites returns references to the prerequisite assertions for the validity of this one. +func (ab *assertionBase) Prerequisites() []*Ref { + return nil +} + +// Ref returns a reference representing this assertion. +func (ab *assertionBase) Ref() *Ref { + assertType := ab.Type() + primKey := make([]string, len(assertType.PrimaryKey)) + for i, name := range assertType.PrimaryKey { + primKey[i] = ab.HeaderString(name) + } + return &Ref{ + Type: assertType, + PrimaryKey: primKey, + } +} + +// sanity check +var _ Assertion = (*assertionBase)(nil) + +// Decode parses a serialized assertion. +// +// The expected serialisation format looks like: +// +// HEADER ("\n\n" BODY?)? "\n\n" SIGNATURE +// +// where: +// +// HEADER is a set of header entries separated by "\n" +// BODY can be arbitrary text, +// SIGNATURE is the signature +// +// Both BODY and HEADER must be UTF8. +// +// A header entry for a single line value (no '\n' in it) looks like: +// +// NAME ": " SIMPLEVALUE +// +// The format supports multiline text values (with '\n's in them) and +// lists or maps, possibly nested, with string scalars in them. +// +// For those a header entry looks like: +// +// NAME ":\n" MULTI(baseindent) +// +// where MULTI can be +// +// * (baseindent + 4)-space indented value (multiline text) +// +// * entries of a list each of the form: +// +// " "*baseindent " -" ( " " SIMPLEVALUE | "\n" MULTI ) +// +// * entries of map each of the form: +// +// " "*baseindent " " NAME ":" ( " " SIMPLEVALUE | "\n" MULTI ) +// +// baseindent starts at 0 and then grows with nesting matching the +// previous level introduction (e.g. the " "*baseindent " -" bit) +// length minus 1. +// +// In general the following headers are mandatory: +// +// type +// authority-id (except for on the wire/self-signed assertions like serial-request) +// +// Further for a given assertion type all the primary key headers +// must be non empty and must not contain '/'. +// +// The following headers expect string representing integer values and +// if omitted otherwise are assumed to be 0: +// +// revision (a positive int) +// body-length (expected to be equal to the length of BODY) +// format (a positive int for the format iteration of the type used) +// +// Times are expected to be in the RFC3339 format: "2006-01-02T15:04:05Z07:00". +// +func Decode(serializedAssertion []byte) (Assertion, error) { + // copy to get an independent backstorage that can't be mutated later + assertionSnapshot := make([]byte, len(serializedAssertion)) + copy(assertionSnapshot, serializedAssertion) + contentSignatureSplit := bytes.LastIndex(assertionSnapshot, nlnl) + if contentSignatureSplit == -1 { + return nil, fmt.Errorf("assertion content/signature separator not found") + } + content := assertionSnapshot[:contentSignatureSplit] + signature := assertionSnapshot[contentSignatureSplit+2:] + + headersBodySplit := bytes.Index(content, nlnl) + var body, head []byte + if headersBodySplit == -1 { + head = content + } else { + body = content[headersBodySplit+2:] + if len(body) == 0 { + body = nil + } + head = content[:headersBodySplit] + } + + headers, err := parseHeaders(head) + if err != nil { + return nil, fmt.Errorf("parsing assertion headers: %v", err) + } + + return assemble(headers, body, content, signature) +} + +// Maximum assertion component sizes. +const ( + MaxBodySize = 2 * 1024 * 1024 + MaxHeadersSize = 128 * 1024 + MaxSignatureSize = 128 * 1024 +) + +// Decoder parses a stream of assertions bundled by separating them with double newlines. +type Decoder struct { + rd io.Reader + initialBufSize int + b *bufio.Reader + err error + maxHeadersSize int + maxSigSize int + + defaultMaxBodySize int + typeMaxBodySize map[*AssertionType]int +} + +// initBuffer finishes a Decoder initialization by setting up the bufio.Reader, +// it returns the *Decoder for convenience of notation. +func (d *Decoder) initBuffer() *Decoder { + d.b = bufio.NewReaderSize(d.rd, d.initialBufSize) + return d +} + +const defaultDecoderBufSize = 4096 + +// NewDecoder returns a Decoder to parse the stream of assertions from the reader. +func NewDecoder(r io.Reader) *Decoder { + return (&Decoder{ + rd: r, + initialBufSize: defaultDecoderBufSize, + maxHeadersSize: MaxHeadersSize, + maxSigSize: MaxSignatureSize, + defaultMaxBodySize: MaxBodySize, + }).initBuffer() +} + +// NewDecoderWithTypeMaxBodySize returns a Decoder to parse the stream of assertions from the reader enforcing optional per type max body sizes or the default one as fallback. +func NewDecoderWithTypeMaxBodySize(r io.Reader, typeMaxBodySize map[*AssertionType]int) *Decoder { + return (&Decoder{ + rd: r, + initialBufSize: defaultDecoderBufSize, + maxHeadersSize: MaxHeadersSize, + maxSigSize: MaxSignatureSize, + defaultMaxBodySize: MaxBodySize, + typeMaxBodySize: typeMaxBodySize, + }).initBuffer() +} + +func (d *Decoder) peek(size int) ([]byte, error) { + buf, err := d.b.Peek(size) + if err == bufio.ErrBufferFull { + rebuf, reerr := d.b.Peek(d.b.Buffered()) + if reerr != nil { + panic(reerr) + } + mr := io.MultiReader(bytes.NewBuffer(rebuf), d.rd) + d.b = bufio.NewReaderSize(mr, (size/d.initialBufSize+1)*d.initialBufSize) + buf, err = d.b.Peek(size) + } + if err != nil && d.err == nil { + d.err = err + } + return buf, d.err +} + +// NB: readExact and readUntil use peek underneath and their returned +// buffers are valid only until the next reading call + +func (d *Decoder) readExact(size int) ([]byte, error) { + buf, err := d.peek(size) + d.b.Discard(len(buf)) + if len(buf) == size { + return buf, nil + } + if err == io.EOF { + return buf, io.ErrUnexpectedEOF + } + return buf, err +} + +func (d *Decoder) readUntil(delim []byte, maxSize int) ([]byte, error) { + last := 0 + size := d.initialBufSize + for { + buf, err := d.peek(size) + if i := bytes.Index(buf[last:], delim); i >= 0 { + d.b.Discard(last + i + len(delim)) + return buf[:last+i+len(delim)], nil + } + // report errors only once we have consumed what is buffered + if err != nil && len(buf) == d.b.Buffered() { + d.b.Discard(len(buf)) + return buf, err + } + last = size - len(delim) + 1 + size *= 2 + if size > maxSize { + return nil, fmt.Errorf("maximum size exceeded while looking for delimiter %q", delim) + } + } +} + +// Decode parses the next assertion from the stream. +// It returns the error io.EOF at the end of a well-formed stream. +func (d *Decoder) Decode() (Assertion, error) { + // read the headers and the nlnl separator after them + headAndSep, err := d.readUntil(nlnl, d.maxHeadersSize) + if err != nil { + if err == io.EOF { + if len(headAndSep) != 0 { + return nil, io.ErrUnexpectedEOF + } + return nil, io.EOF + } + return nil, fmt.Errorf("error reading assertion headers: %v", err) + } + + headLen := len(headAndSep) - len(nlnl) + headers, err := parseHeaders(headAndSep[:headLen]) + if err != nil { + return nil, fmt.Errorf("parsing assertion headers: %v", err) + } + + typeStr, _ := headers["type"].(string) + typ := Type(typeStr) + + length, err := checkIntWithDefault(headers, "body-length", 0) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + if typMaxBodySize := d.typeMaxBodySize[typ]; typMaxBodySize != 0 && length > typMaxBodySize { + return nil, fmt.Errorf("assertion body length %d exceeds maximum body size %d for %q assertions", length, typMaxBodySize, typ.Name) + } else if length > d.defaultMaxBodySize { + return nil, fmt.Errorf("assertion body length %d exceeds maximum body size", length) + } + + // save the headers before we try to read more, and setup to capture + // the whole content in a buffer + contentBuf := bytes.NewBuffer(make([]byte, 0, len(headAndSep)+length)) + contentBuf.Write(headAndSep) + + if length > 0 { + // read the body if length != 0 + body, err := d.readExact(length) + if err != nil { + return nil, err + } + contentBuf.Write(body) + } + + // try to read the end of body a.k.a content/signature separator + endOfBody, err := d.readUntil(nlnl, d.maxSigSize) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("error reading assertion trailer: %v", err) + } + + var sig []byte + if bytes.Equal(endOfBody, nlnl) { + // we got the nlnl content/signature separator, read the signature now and the assertion/assertion nlnl separation + sig, err = d.readUntil(nlnl, d.maxSigSize) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("error reading assertion signature: %v", err) + } + } else { + // we got the signature directly which is a ok format only if body length == 0 + if length > 0 { + return nil, fmt.Errorf("missing content/signature separator") + } + sig = endOfBody + contentBuf.Truncate(headLen) + } + + // normalize sig ending newlines + if bytes.HasSuffix(sig, nlnl) { + sig = sig[:len(sig)-1] + } + + finalContent := contentBuf.Bytes() + var finalBody []byte + if length > 0 { + finalBody = finalContent[headLen+len(nlnl):] + } + + finalSig := make([]byte, len(sig)) + copy(finalSig, sig) + + return assemble(headers, finalBody, finalContent, finalSig) +} + +func checkIteration(headers map[string]interface{}, name string) (int, error) { + iternum, err := checkIntWithDefault(headers, name, 0) + if err != nil { + return -1, err + } + if iternum < 0 { + return -1, fmt.Errorf("%s should be positive: %v", name, iternum) + } + return iternum, nil +} + +func checkFormat(headers map[string]interface{}) (int, error) { + return checkIteration(headers, "format") +} + +func checkRevision(headers map[string]interface{}) (int, error) { + return checkIteration(headers, "revision") +} + +// Assemble assembles an assertion from its components. +func Assemble(headers map[string]interface{}, body, content, signature []byte) (Assertion, error) { + err := checkHeaders(headers) + if err != nil { + return nil, err + } + return assemble(headers, body, content, signature) +} + +// assemble is the internal variant of Assemble, assumes headers are already checked for supported types +func assemble(headers map[string]interface{}, body, content, signature []byte) (Assertion, error) { + length, err := checkIntWithDefault(headers, "body-length", 0) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + if length != len(body) { + return nil, fmt.Errorf("assertion body length and declared body-length don't match: %v != %v", len(body), length) + } + + if !utf8.Valid(body) { + return nil, fmt.Errorf("body is not utf8") + } + + if _, err := checkDigest(headers, "sign-key-sha3-384", crypto.SHA3_384); err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + + typ, err := checkNotEmptyString(headers, "type") + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + assertType := Type(typ) + if assertType == nil { + return nil, fmt.Errorf("unknown assertion type: %q", typ) + } + + if assertType.flags&noAuthority == 0 { + if _, err := checkNotEmptyString(headers, "authority-id"); err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + } else { + _, ok := headers["authority-id"] + if ok { + return nil, fmt.Errorf("%q assertion cannot have authority-id set", assertType.Name) + } + } + + formatnum, err := checkFormat(headers) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + + for _, primKey := range assertType.PrimaryKey { + if _, err := checkPrimaryKey(headers, primKey); err != nil { + return nil, fmt.Errorf("assertion %s: %v", assertType.Name, err) + } + } + + revision, err := checkRevision(headers) + if err != nil { + return nil, fmt.Errorf("assertion: %v", err) + } + + if len(signature) == 0 { + return nil, fmt.Errorf("empty assertion signature") + } + + assert, err := assertType.assembler(assertionBase{ + headers: headers, + body: body, + format: formatnum, + revision: revision, + content: content, + signature: signature, + }) + if err != nil { + return nil, fmt.Errorf("assertion %s: %v", assertType.Name, err) + } + return assert, nil +} + +func writeHeader(buf *bytes.Buffer, headers map[string]interface{}, name string) { + appendEntry(buf, fmt.Sprintf("%s:", name), headers[name], 0) +} + +func assembleAndSign(assertType *AssertionType, headers map[string]interface{}, body []byte, privKey PrivateKey) (Assertion, error) { + err := checkAssertType(assertType) + if err != nil { + return nil, err + } + + withAuthority := assertType.flags&noAuthority == 0 + + err = checkHeaders(headers) + if err != nil { + return nil, err + } + + // there's no hint at all that we will need non-textual bodies, + // make sure we actually enforce that + if !utf8.Valid(body) { + return nil, fmt.Errorf("assertion body is not utf8") + } + + finalHeaders := copyHeaders(headers) + bodyLength := len(body) + finalBody := make([]byte, bodyLength) + copy(finalBody, body) + finalHeaders["type"] = assertType.Name + finalHeaders["body-length"] = strconv.Itoa(bodyLength) + finalHeaders["sign-key-sha3-384"] = privKey.PublicKey().ID() + + if withAuthority { + if _, err := checkNotEmptyString(finalHeaders, "authority-id"); err != nil { + return nil, err + } + } else { + _, ok := finalHeaders["authority-id"] + if ok { + return nil, fmt.Errorf("%q assertion cannot have authority-id set", assertType.Name) + } + } + + formatnum, err := checkFormat(finalHeaders) + if err != nil { + return nil, err + } + + if formatnum > assertType.MaxSupportedFormat() { + return nil, fmt.Errorf("cannot sign %q assertion with format %d higher than max supported format %d", assertType.Name, formatnum, assertType.MaxSupportedFormat()) + } + + suggestedFormat, err := SuggestFormat(assertType, finalHeaders, finalBody) + if err != nil { + return nil, err + } + + if suggestedFormat > formatnum { + return nil, fmt.Errorf("cannot sign %q assertion with format set to %d lower than min format %d covering included features", assertType.Name, formatnum, suggestedFormat) + } + + revision, err := checkRevision(finalHeaders) + if err != nil { + return nil, err + } + + buf := bytes.NewBufferString("type: ") + buf.WriteString(assertType.Name) + + if formatnum > 0 { + writeHeader(buf, finalHeaders, "format") + } else { + delete(finalHeaders, "format") + } + + if withAuthority { + writeHeader(buf, finalHeaders, "authority-id") + } + + if revision > 0 { + writeHeader(buf, finalHeaders, "revision") + } else { + delete(finalHeaders, "revision") + } + written := map[string]bool{ + "type": true, + "format": true, + "authority-id": true, + "revision": true, + "body-length": true, + "sign-key-sha3-384": true, + } + for _, primKey := range assertType.PrimaryKey { + if _, err := checkPrimaryKey(finalHeaders, primKey); err != nil { + return nil, err + } + writeHeader(buf, finalHeaders, primKey) + written[primKey] = true + } + + // emit other headers in lexicographic order + otherKeys := make([]string, 0, len(finalHeaders)) + for name := range finalHeaders { + if !written[name] { + otherKeys = append(otherKeys, name) + } + } + sort.Strings(otherKeys) + for _, k := range otherKeys { + writeHeader(buf, finalHeaders, k) + } + + // body-length and body + if bodyLength > 0 { + writeHeader(buf, finalHeaders, "body-length") + } else { + delete(finalHeaders, "body-length") + } + + // signing key reference + writeHeader(buf, finalHeaders, "sign-key-sha3-384") + + if bodyLength > 0 { + buf.Grow(bodyLength + 2) + buf.Write(nlnl) + buf.Write(finalBody) + } else { + finalBody = nil + } + content := buf.Bytes() + + signature, err := signContent(content, privKey) + if err != nil { + return nil, fmt.Errorf("cannot sign assertion: %v", err) + } + // be 'cat' friendly, add a ignored newline to the signature which is the last part of the encoded assertion + signature = append(signature, '\n') + + assert, err := assertType.assembler(assertionBase{ + headers: finalHeaders, + body: finalBody, + format: formatnum, + revision: revision, + content: content, + signature: signature, + }) + if err != nil { + return nil, fmt.Errorf("cannot assemble assertion %s: %v", assertType.Name, err) + } + return assert, nil +} + +// SignWithoutAuthority assembles an assertion without a set authority with the provided information and signs it with the given private key. +func SignWithoutAuthority(assertType *AssertionType, headers map[string]interface{}, body []byte, privKey PrivateKey) (Assertion, error) { + if assertType.flags&noAuthority == 0 { + return nil, fmt.Errorf("cannot sign assertions needing a definite authority with SignWithoutAuthority") + } + return assembleAndSign(assertType, headers, body, privKey) +} + +// Encode serializes an assertion. +func Encode(assert Assertion) []byte { + content, signature := assert.Signature() + needed := len(content) + 2 + len(signature) + buf := bytes.NewBuffer(make([]byte, 0, needed)) + buf.Write(content) + buf.Write(nlnl) + buf.Write(signature) + return buf.Bytes() +} + +// Encoder emits a stream of assertions bundled by separating them with double newlines. +type Encoder struct { + wr io.Writer + nextSep []byte +} + +// NewEncoder returns a Encoder to emit a stream of assertions to a writer. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{wr: w} +} + +func (enc *Encoder) writeSep(last byte) error { + if last != '\n' { + _, err := enc.wr.Write(nl) + if err != nil { + return err + } + } + enc.nextSep = nl + return nil +} + +// WriteEncoded writes the encoded assertion into the stream with the required separator. +func (enc *Encoder) WriteEncoded(encoded []byte) error { + sz := len(encoded) + if sz == 0 { + return fmt.Errorf("internal error: encoded assertion cannot be empty") + } + + _, err := enc.wr.Write(enc.nextSep) + if err != nil { + return err + } + + _, err = enc.wr.Write(encoded) + if err != nil { + return err + } + + return enc.writeSep(encoded[sz-1]) +} + +// WriteContentSignature writes the content and signature of an assertion into the stream with all the required separators. +func (enc *Encoder) WriteContentSignature(content, signature []byte) error { + if len(content) == 0 { + return fmt.Errorf("internal error: content cannot be empty") + } + + sz := len(signature) + if sz == 0 { + return fmt.Errorf("internal error: signature cannot be empty") + } + + _, err := enc.wr.Write(enc.nextSep) + if err != nil { + return err + } + + _, err = enc.wr.Write(content) + if err != nil { + return err + } + _, err = enc.wr.Write(nlnl) + if err != nil { + return err + } + _, err = enc.wr.Write(signature) + if err != nil { + return err + } + + return enc.writeSep(signature[sz-1]) +} + +// Encode emits the assertion into the stream with the required separator. +// Errors here are always about writing given that Encode() itself cannot error. +func (enc *Encoder) Encode(assert Assertion) error { + return enc.WriteContentSignature(assert.Signature()) +} + +// SignatureCheck checks the signature of the assertion against the given public key. Useful for assertions with no authority. +func SignatureCheck(assert Assertion, pubKey PublicKey) error { + content, encodedSig := assert.Signature() + sig, err := decodeSignature(encodedSig) + if err != nil { + return err + } + err = pubKey.verify(content, sig) + if err != nil { + return fmt.Errorf("failed signature verification: %v", err) + } + return nil +} diff --git a/asserts/asserts_test.go b/asserts/asserts_test.go new file mode 100644 index 00000000..ccc143e7 --- /dev/null +++ b/asserts/asserts_test.go @@ -0,0 +1,899 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "bytes" + "io" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type assertsSuite struct{} + +var _ = Suite(&assertsSuite{}) + +func (as *assertsSuite) TestType(c *C) { + c.Check(asserts.Type("test-only"), Equals, asserts.TestOnlyType) +} + +func (as *assertsSuite) TestUnknown(c *C) { + c.Check(asserts.Type(""), IsNil) + c.Check(asserts.Type("unknown"), IsNil) +} + +func (as *assertsSuite) TestTypeMaxSupportedFormat(c *C) { + c.Check(asserts.Type("test-only").MaxSupportedFormat(), Equals, 1) +} + +func (as *assertsSuite) TestTypeNames(c *C) { + c.Check(asserts.TypeNames(), DeepEquals, []string{ + "account", + "account-key", + "account-key-request", + "base-declaration", + "device-session-request", + "model", + "repair", + "serial", + "serial-request", + "snap-build", + "snap-declaration", + "snap-developer", + "snap-revision", + "store", + "system-user", + "test-only", + "test-only-2", + "test-only-no-authority", + "test-only-no-authority-pk", + "validation", + }) +} + +func (as *assertsSuite) TestSuggestFormat(c *C) { + fmtnum, err := asserts.SuggestFormat(asserts.Type("test-only-2"), nil, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 0) +} + +func (as *assertsSuite) TestPrimaryKeyHelpers(c *C) { + headers, err := asserts.HeadersFromPrimaryKey(asserts.TestOnlyType, []string{"one"}) + c.Assert(err, IsNil) + c.Check(headers, DeepEquals, map[string]string{ + "primary-key": "one", + }) + + headers, err = asserts.HeadersFromPrimaryKey(asserts.TestOnly2Type, []string{"bar", "baz"}) + c.Assert(err, IsNil) + c.Check(headers, DeepEquals, map[string]string{ + "pk1": "bar", + "pk2": "baz", + }) + + _, err = asserts.HeadersFromPrimaryKey(asserts.TestOnly2Type, []string{"bar"}) + c.Check(err, ErrorMatches, `primary key has wrong length for "test-only-2" assertion`) + + _, err = asserts.HeadersFromPrimaryKey(asserts.TestOnly2Type, []string{"", "baz"}) + c.Check(err, ErrorMatches, `primary key "pk1" header cannot be empty`) + + pk, err := asserts.PrimaryKeyFromHeaders(asserts.TestOnly2Type, headers) + c.Assert(err, IsNil) + c.Check(pk, DeepEquals, []string{"bar", "baz"}) + + headers["other"] = "foo" + pk1, err := asserts.PrimaryKeyFromHeaders(asserts.TestOnly2Type, headers) + c.Assert(err, IsNil) + c.Check(pk1, DeepEquals, pk) + + delete(headers, "pk2") + _, err = asserts.PrimaryKeyFromHeaders(asserts.TestOnly2Type, headers) + c.Check(err, ErrorMatches, `must provide primary key: pk2`) +} + +func (as *assertsSuite) TestRef(c *C) { + ref := &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz") +} + +func (as *assertsSuite) TestRefString(c *C) { + ref := &asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"canonical"}, + } + + c.Check(ref.String(), Equals, "account (canonical)") + + ref = &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{"18", "SNAPID"}, + } + + c.Check(ref.String(), Equals, "snap-declaration (SNAPID; series:18)") + + ref = &asserts.Ref{ + Type: asserts.ModelType, + PrimaryKey: []string{"18", "BRAND", "baz-3000"}, + } + + c.Check(ref.String(), Equals, "model (baz-3000; series:18 brand-id:BRAND)") + + // broken primary key + ref = &asserts.Ref{ + Type: asserts.ModelType, + PrimaryKey: []string{"18"}, + } + c.Check(ref.String(), Equals, "model (???)") + + ref = &asserts.Ref{ + Type: asserts.TestOnlyNoAuthorityType, + } + c.Check(ref.String(), Equals, "test-only-no-authority (-)") +} + +func (as *assertsSuite) TestRefResolveError(c *C) { + ref := &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc"}, + } + _, err := ref.Resolve(nil) + c.Check(err, ErrorMatches, `"test-only-2" assertion reference primary key has the wrong length \(expected \[pk1 pk2\]\): \[abc\]`) +} + +const exKeyID = "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + +const exampleEmptyBodyAllDefaults = "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: abc\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + +func (as *assertsSuite) TestDecodeEmptyBodyAllDefaults(c *C) { + a, err := asserts.Decode([]byte(exampleEmptyBodyAllDefaults)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + _, ok := a.(*asserts.TestOnly) + c.Check(ok, Equals, true) + c.Check(a.Revision(), Equals, 0) + c.Check(a.Format(), Equals, 0) + c.Check(a.Body(), IsNil) + c.Check(a.Header("header1"), IsNil) + c.Check(a.HeaderString("header1"), Equals, "") + c.Check(a.AuthorityID(), Equals, "auth-id1") + c.Check(a.SignKeyID(), Equals, exKeyID) +} + +const exampleEmptyBody2NlNl = "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: xyz\n" + + "revision: 0\n" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "\n\n" + + "AXNpZw==\n" + +func (as *assertsSuite) TestDecodeEmptyBodyNormalize2NlNl(c *C) { + a, err := asserts.Decode([]byte(exampleEmptyBody2NlNl)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + c.Check(a.Revision(), Equals, 0) + c.Check(a.Format(), Equals, 0) + c.Check(a.Body(), IsNil) +} + +const exampleBodyAndExtraHeaders = "type: test-only\n" + + "format: 1\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==\n" + +func (as *assertsSuite) TestDecodeWithABodyAndExtraHeaders(c *C) { + a, err := asserts.Decode([]byte(exampleBodyAndExtraHeaders)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + c.Check(a.AuthorityID(), Equals, "auth-id2") + c.Check(a.SignKeyID(), Equals, exKeyID) + c.Check(a.Header("primary-key"), Equals, "abc") + c.Check(a.Revision(), Equals, 5) + c.Check(a.Format(), Equals, 1) + c.Check(a.SupportedFormat(), Equals, true) + c.Check(a.Header("header1"), Equals, "value1") + c.Check(a.Header("header2"), Equals, "value2") + c.Check(a.Body(), DeepEquals, []byte("THE-BODY")) + +} + +const exampleUnsupportedFormat = "type: test-only\n" + + "format: 77\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "AXNpZw==\n" + +func (as *assertsSuite) TestDecodeUnsupportedFormat(c *C) { + a, err := asserts.Decode([]byte(exampleUnsupportedFormat)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + c.Check(a.AuthorityID(), Equals, "auth-id2") + c.Check(a.SignKeyID(), Equals, exKeyID) + c.Check(a.Header("primary-key"), Equals, "abc") + c.Check(a.Revision(), Equals, 5) + c.Check(a.Format(), Equals, 77) + c.Check(a.SupportedFormat(), Equals, false) +} + +func (as *assertsSuite) TestDecodeGetSignatureBits(c *C) { + content := "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: xyz\n" + + "revision: 5\n" + + "header1: value1\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + encoded := content + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + c.Check(a.AuthorityID(), Equals, "auth-id1") + c.Check(a.SignKeyID(), Equals, exKeyID) + cont, signature := a.Signature() + c.Check(signature, DeepEquals, []byte("AXNpZw==")) + c.Check(cont, DeepEquals, []byte(content)) +} + +func (as *assertsSuite) TestDecodeNoSignatureSplit(c *C) { + for _, encoded := range []string{"", "foo"} { + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, ErrorMatches, "assertion content/signature separator not found") + } +} + +func (as *assertsSuite) TestDecodeHeaderParsingErrors(c *C) { + headerParsingErrorsTests := []struct{ encoded, expectedErr string }{ + {string([]byte{255, '\n', '\n'}), "header is not utf8"}, + {"foo: a\nbar\n\n", `header entry missing ':' separator: "bar"`}, + {"TYPE: foo\n\n", `invalid header name: "TYPE"`}, + {"foo: a\nbar:>\n\n", `header entry should have a space or newline \(for multiline\) before value: "bar:>"`}, + {"foo: a\nbar:\n\n", `expected 4 chars nesting prefix after multiline introduction "bar:": EOF`}, + {"foo: a\nbar:\nbaz: x\n\n", `expected 4 chars nesting prefix after multiline introduction "bar:": "baz: x"`}, + {"foo: a:\nbar: b\nfoo: x\n\n", `repeated header: "foo"`}, + } + + for _, test := range headerParsingErrorsTests { + _, err := asserts.Decode([]byte(test.encoded)) + c.Check(err, ErrorMatches, "parsing assertion headers: "+test.expectedErr) + } +} + +func (as *assertsSuite) TestDecodeInvalid(c *C) { + keyIDHdr := "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n" + encoded := "type: test-only\n" + + "format: 0\n" + + "authority-id: auth-id\n" + + "primary-key: abc\n" + + "revision: 0\n" + + "body-length: 5\n" + + keyIDHdr + + "\n" + + "abcde" + + "\n\n" + + "AXNpZw==" + + invalidAssertTests := []struct{ original, invalid, expectedErr string }{ + {"body-length: 5", "body-length: z", `assertion: "body-length" header is not an integer: z`}, + {"body-length: 5", "body-length: 3", "assertion body length and declared body-length don't match: 5 != 3"}, + {"authority-id: auth-id\n", "", `assertion: "authority-id" header is mandatory`}, + {"authority-id: auth-id\n", "authority-id: \n", `assertion: "authority-id" header should not be empty`}, + {keyIDHdr, "", `assertion: "sign-key-sha3-384" header is mandatory`}, + {keyIDHdr, "sign-key-sha3-384: \n", `assertion: "sign-key-sha3-384" header should not be empty`}, + {keyIDHdr, "sign-key-sha3-384: $\n", `assertion: "sign-key-sha3-384" header cannot be decoded: .*`}, + {keyIDHdr, "sign-key-sha3-384: eHl6\n", `assertion: "sign-key-sha3-384" header does not have the expected bit length: 24`}, + {"AXNpZw==", "", "empty assertion signature"}, + {"type: test-only\n", "", `assertion: "type" header is mandatory`}, + {"type: test-only\n", "type: unknown\n", `unknown assertion type: "unknown"`}, + {"revision: 0\n", "revision: Z\n", `assertion: "revision" header is not an integer: Z`}, + {"revision: 0\n", "revision:\n - 1\n", `assertion: "revision" header is not an integer: \[1\]`}, + {"revision: 0\n", "revision: -10\n", "assertion: revision should be positive: -10"}, + {"format: 0\n", "format: Z\n", `assertion: "format" header is not an integer: Z`}, + {"format: 0\n", "format: -10\n", "assertion: format should be positive: -10"}, + {"primary-key: abc\n", "", `assertion test-only: "primary-key" header is mandatory`}, + {"primary-key: abc\n", "primary-key:\n - abc\n", `assertion test-only: "primary-key" header must be a string`}, + {"primary-key: abc\n", "primary-key: a/c\n", `assertion test-only: "primary-key" primary key header cannot contain '/'`}, + {"abcde", "ab\xffde", "body is not utf8"}, + } + + for _, test := range invalidAssertTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, test.expectedErr) + } +} + +func (as *assertsSuite) TestDecodeNoAuthorityInvalid(c *C) { + invalid := "type: test-only-no-authority\n" + + "authority-id: auth-id1\n" + + "hdr: FOO\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "openpgp c2ln" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, `"test-only-no-authority" assertion cannot have authority-id set`) +} + +func checkContent(c *C, a asserts.Assertion, encoded string) { + expected, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + expectedCont, _ := expected.Signature() + + cont, _ := a.Signature() + c.Check(cont, DeepEquals, expectedCont) +} + +func (as *assertsSuite) TestEncoderDecoderHappy(c *C) { + stream := new(bytes.Buffer) + enc := asserts.NewEncoder(stream) + enc.WriteEncoded([]byte(exampleEmptyBody2NlNl)) + enc.WriteEncoded([]byte(exampleBodyAndExtraHeaders)) + enc.WriteEncoded([]byte(exampleEmptyBodyAllDefaults)) + + decoder := asserts.NewDecoder(stream) + a, err := decoder.Decode() + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + _, ok := a.(*asserts.TestOnly) + c.Check(ok, Equals, true) + checkContent(c, a, exampleEmptyBody2NlNl) + + a, err = decoder.Decode() + c.Assert(err, IsNil) + checkContent(c, a, exampleBodyAndExtraHeaders) + + a, err = decoder.Decode() + c.Assert(err, IsNil) + checkContent(c, a, exampleEmptyBodyAllDefaults) + + a, err = decoder.Decode() + c.Assert(err, Equals, io.EOF) +} + +func (as *assertsSuite) TestDecodeEmptyStream(c *C) { + stream := new(bytes.Buffer) + decoder := asserts.NewDecoder(stream) + _, err := decoder.Decode() + c.Check(err, Equals, io.EOF) +} + +func (as *assertsSuite) TestDecoderHappyWithSeparatorsVariations(c *C) { + streams := []string{ + exampleBodyAndExtraHeaders, + exampleEmptyBody2NlNl, + exampleEmptyBodyAllDefaults, + } + + for _, streamData := range streams { + stream := bytes.NewBufferString(streamData) + decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024) + a, err := decoder.Decode() + c.Assert(err, IsNil, Commentf("stream: %q", streamData)) + + checkContent(c, a, streamData) + + a, err = decoder.Decode() + c.Check(a, IsNil) + c.Check(err, Equals, io.EOF, Commentf("stream: %q", streamData)) + } +} + +func (as *assertsSuite) TestDecoderHappyWithTrailerDoubleNewlines(c *C) { + streams := []string{ + exampleBodyAndExtraHeaders, + exampleEmptyBody2NlNl, + exampleEmptyBodyAllDefaults, + } + + for _, streamData := range streams { + stream := bytes.NewBufferString(streamData) + if strings.HasSuffix(streamData, "\n") { + stream.WriteString("\n") + } else { + stream.WriteString("\n\n") + } + + decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024) + a, err := decoder.Decode() + c.Assert(err, IsNil, Commentf("stream: %q", streamData)) + + checkContent(c, a, streamData) + + a, err = decoder.Decode() + c.Check(a, IsNil) + c.Check(err, Equals, io.EOF, Commentf("stream: %q", streamData)) + } +} + +func (as *assertsSuite) TestDecoderUnexpectedEOF(c *C) { + streamData := exampleBodyAndExtraHeaders + "\n" + exampleEmptyBodyAllDefaults + fstHeadEnd := strings.Index(exampleBodyAndExtraHeaders, "\n\n") + sndHeadEnd := len(exampleBodyAndExtraHeaders) + 1 + strings.Index(exampleEmptyBodyAllDefaults, "\n\n") + + for _, brk := range []int{1, fstHeadEnd / 2, fstHeadEnd, fstHeadEnd + 1, fstHeadEnd + 2, fstHeadEnd + 6} { + stream := bytes.NewBufferString(streamData[:brk]) + decoder := asserts.NewDecoderStressed(stream, 16, 1024, 1024, 1024) + _, err := decoder.Decode() + c.Check(err, Equals, io.ErrUnexpectedEOF, Commentf("brk: %d", brk)) + } + + for _, brk := range []int{sndHeadEnd, sndHeadEnd + 1} { + stream := bytes.NewBufferString(streamData[:brk]) + decoder := asserts.NewDecoder(stream) + _, err := decoder.Decode() + c.Assert(err, IsNil) + + _, err = decoder.Decode() + c.Check(err, Equals, io.ErrUnexpectedEOF, Commentf("brk: %d", brk)) + } +} + +func (as *assertsSuite) TestDecoderBrokenBodySeparation(c *C) { + streamData := strings.Replace(exampleBodyAndExtraHeaders, "THE-BODY\n\n", "THE-BODY", 1) + decoder := asserts.NewDecoder(bytes.NewBufferString(streamData)) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, "missing content/signature separator") + + streamData = strings.Replace(exampleBodyAndExtraHeaders, "THE-BODY\n\n", "THE-BODY\n", 1) + decoder = asserts.NewDecoder(bytes.NewBufferString(streamData)) + _, err = decoder.Decode() + c.Assert(err, ErrorMatches, "missing content/signature separator") +} + +func (as *assertsSuite) TestDecoderHeadTooBig(c *C) { + decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 4, 4, 1024, 1024) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, `error reading assertion headers: maximum size exceeded while looking for delimiter "\\n\\n"`) +} + +func (as *assertsSuite) TestDecoderBodyTooBig(c *C) { + decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 1024, 1024, 5, 1024) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, "assertion body length 8 exceeds maximum body size") +} + +func (as *assertsSuite) TestDecoderSignatureTooBig(c *C) { + decoder := asserts.NewDecoderStressed(bytes.NewBufferString(exampleBodyAndExtraHeaders), 4, 1024, 1024, 7) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, `error reading assertion signature: maximum size exceeded while looking for delimiter "\\n\\n"`) +} + +func (as *assertsSuite) TestDecoderDefaultMaxBodySize(c *C) { + enc := strings.Replace(exampleBodyAndExtraHeaders, "body-length: 8", "body-length: 2097153", 1) + decoder := asserts.NewDecoder(bytes.NewBufferString(enc)) + _, err := decoder.Decode() + c.Assert(err, ErrorMatches, "assertion body length 2097153 exceeds maximum body size") +} + +func (as *assertsSuite) TestDecoderWithTypeMaxBodySize(c *C) { + ex1 := strings.Replace(exampleBodyAndExtraHeaders, "body-length: 8", "body-length: 2097152", 1) + ex1 = strings.Replace(ex1, "THE-BODY", strings.Repeat("B", 2*1024*1024), 1) + ex1toobig := strings.Replace(exampleBodyAndExtraHeaders, "body-length: 8", "body-length: 2097153", 1) + ex1toobig = strings.Replace(ex1toobig, "THE-BODY", strings.Repeat("B", 2*1024*1024+1), 1) + const ex2 = `type: test-only-2 +authority-id: auth-id1 +pk1: foo +pk2: bar +body-length: 3 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +XYZ + +AXNpZw==` + + decoder := asserts.NewDecoderWithTypeMaxBodySize(bytes.NewBufferString(ex1+"\n"+ex2), map[*asserts.AssertionType]int{ + asserts.TestOnly2Type: 3, + }) + a1, err := decoder.Decode() + c.Assert(err, IsNil) + c.Check(a1.Body(), HasLen, 2*1024*1024) + a2, err := decoder.Decode() + c.Assert(err, IsNil) + c.Check(a2.Body(), DeepEquals, []byte("XYZ")) + + decoder = asserts.NewDecoderWithTypeMaxBodySize(bytes.NewBufferString(ex1+"\n"+ex2), map[*asserts.AssertionType]int{ + asserts.TestOnly2Type: 2, + }) + a1, err = decoder.Decode() + c.Assert(err, IsNil) + c.Check(a1.Body(), HasLen, 2*1024*1024) + _, err = decoder.Decode() + c.Assert(err, ErrorMatches, `assertion body length 3 exceeds maximum body size 2 for "test-only-2" assertions`) + + decoder = asserts.NewDecoderWithTypeMaxBodySize(bytes.NewBufferString(ex2+"\n\n"+ex1toobig), map[*asserts.AssertionType]int{ + asserts.TestOnly2Type: 3, + }) + a2, err = decoder.Decode() + c.Assert(err, IsNil) + c.Check(a2.Body(), DeepEquals, []byte("XYZ")) + _, err = decoder.Decode() + c.Assert(err, ErrorMatches, "assertion body length 2097153 exceeds maximum body size") +} + +func (as *assertsSuite) TestEncode(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: xyz\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + encodeRes := asserts.Encode(a) + c.Check(encodeRes, DeepEquals, encoded) +} + +func (as *assertsSuite) TestEncoderOK(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: xyzyz\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a0, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + cont0, _ := a0.Signature() + + stream := new(bytes.Buffer) + enc := asserts.NewEncoder(stream) + enc.Encode(a0) + + c.Check(bytes.HasSuffix(stream.Bytes(), []byte{'\n'}), Equals, true) + + dec := asserts.NewDecoder(stream) + a1, err := dec.Decode() + c.Assert(err, IsNil) + + cont1, _ := a1.Signature() + c.Check(cont1, DeepEquals, cont0) +} + +func (as *assertsSuite) TestEncoderSingleDecodeOK(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a0, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + cont0, _ := a0.Signature() + + stream := new(bytes.Buffer) + enc := asserts.NewEncoder(stream) + enc.Encode(a0) + + a1, err := asserts.Decode(stream.Bytes()) + c.Assert(err, IsNil) + + cont1, _ := a1.Signature() + c.Check(cont1, DeepEquals, cont0) +} + +func (as *assertsSuite) TestSignFormatSanityEmptyBody(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + _, err = asserts.Decode(asserts.Encode(a)) + c.Check(err, IsNil) +} + +func (as *assertsSuite) TestSignFormatSanityNonEmptyBody(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + body := []byte("THE-BODY") + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, body, testPrivKey1) + c.Assert(err, IsNil) + c.Check(a.Body(), DeepEquals, body) + + decoded, err := asserts.Decode(asserts.Encode(a)) + c.Assert(err, IsNil) + c.Check(decoded.Body(), DeepEquals, body) +} + +func (as *assertsSuite) TestSignFormatSanitySupportMultilineHeaderValues(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + + multilineVals := []string{ + "a\n", + "\na", + "a\n\b\nc", + "a\n\b\nc\n", + "\na\n", + "\n\na\n\nb\n\nc", + } + + for _, multilineVal := range multilineVals { + headers["multiline"] = multilineVal + if len(multilineVal)%2 == 1 { + headers["odd"] = "true" + } + + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + decoded, err := asserts.Decode(asserts.Encode(a)) + c.Assert(err, IsNil) + + c.Check(decoded.Header("multiline"), Equals, multilineVal) + } +} + +func (as *assertsSuite) TestSignFormatAndRevision(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + "format": "1", + "revision": "11", + } + + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + c.Check(a.Revision(), Equals, 11) + c.Check(a.Format(), Equals, 1) + c.Check(a.SupportedFormat(), Equals, true) + + a1, err := asserts.Decode(asserts.Encode(a)) + c.Assert(err, IsNil) + + c.Check(a1.Revision(), Equals, 11) + c.Check(a1.Format(), Equals, 1) + c.Check(a1.SupportedFormat(), Equals, true) +} + +func (as *assertsSuite) TestSignBodyIsUTF8Text(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + _, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, []byte{'\xff'}, testPrivKey1) + c.Assert(err, ErrorMatches, "assertion body is not utf8") +} + +func (as *assertsSuite) TestHeaders(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + + hs := a.Headers() + c.Check(hs, DeepEquals, map[string]interface{}{ + "type": "test-only", + "authority-id": "auth-id2", + "primary-key": "abc", + "revision": "5", + "header1": "value1", + "header2": "value2", + "body-length": "8", + "sign-key-sha3-384": exKeyID, + }) +} + +func (as *assertsSuite) TestHeadersReturnsCopy(c *C) { + encoded := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: xyz\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + + hs := a.Headers() + // casual later result mutation doesn't trip us + delete(hs, "primary-key") + c.Check(a.Header("primary-key"), Equals, "xyz") +} + +func (as *assertsSuite) TestAssembleRoundtrip(c *C) { + encoded := []byte("type: test-only\n" + + "format: 1\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5\n" + + "header1: value1\n" + + "header2: value2\n" + + "body-length: 8\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "THE-BODY" + + "\n\n" + + "AXNpZw==") + a, err := asserts.Decode(encoded) + c.Assert(err, IsNil) + + cont, sig := a.Signature() + reassembled, err := asserts.Assemble(a.Headers(), a.Body(), cont, sig) + c.Assert(err, IsNil) + + c.Check(reassembled.Headers(), DeepEquals, a.Headers()) + c.Check(reassembled.Body(), DeepEquals, a.Body()) + + reassembledEncoded := asserts.Encode(reassembled) + c.Check(reassembledEncoded, DeepEquals, encoded) +} + +func (as *assertsSuite) TestSignKeyID(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + keyID := a.SignKeyID() + c.Check(keyID, Equals, testPrivKey1.PublicKey().ID()) +} + +func (as *assertsSuite) TestSelfRef(c *C) { + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "0", + } + a1, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + c.Check(a1.Ref(), DeepEquals, &asserts.Ref{ + Type: asserts.TestOnlyType, + PrimaryKey: []string{"0"}, + }) + + headers = map[string]interface{}{ + "authority-id": "auth-id1", + "pk1": "a", + "pk2": "b", + } + a2, err := asserts.AssembleAndSignInTest(asserts.TestOnly2Type, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + c.Check(a2.Ref(), DeepEquals, &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"a", "b"}, + }) +} + +func (as *assertsSuite) TestAssembleHeadersCheck(c *C) { + cont := []byte("type: test-only\n" + + "authority-id: auth-id2\n" + + "primary-key: abc\n" + + "revision: 5") + headers := map[string]interface{}{ + "type": "test-only", + "authority-id": "auth-id2", + "primary-key": "abc", + "revision": 5, // must be a string actually! + } + + _, err := asserts.Assemble(headers, nil, cont, nil) + c.Check(err, ErrorMatches, `header "revision": header values must be strings or nested lists or maps with strings as the only scalars: 5`) +} + +func (as *assertsSuite) TestSignWithoutAuthorityMisuse(c *C) { + _, err := asserts.SignWithoutAuthority(asserts.TestOnlyType, nil, nil, testPrivKey1) + c.Check(err, ErrorMatches, `cannot sign assertions needing a definite authority with SignWithoutAuthority`) + + _, err = asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType, + map[string]interface{}{ + "authority-id": "auth-id1", + "hdr": "FOO", + }, nil, testPrivKey1) + c.Check(err, ErrorMatches, `"test-only-no-authority" assertion cannot have authority-id set`) +} + +func (ss *serialSuite) TestSignatureCheckError(c *C) { + sreq, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType, + map[string]interface{}{ + "hdr": "FOO", + }, nil, testPrivKey1) + c.Assert(err, IsNil) + + err = asserts.SignatureCheck(sreq, testPrivKey2.PublicKey()) + c.Check(err, ErrorMatches, `failed signature verification:.*`) +} + +func (as *assertsSuite) TestWithAuthority(c *C) { + withAuthority := []string{ + "account", + "account-key", + "base-declaration", + "store", + "snap-declaration", + "snap-build", + "snap-revision", + "snap-developer", + "model", + "serial", + "system-user", + "validation", + "repair", + } + c.Check(withAuthority, HasLen, asserts.NumAssertionType-3) // excluding device-session-request, serial-request, account-key-request + for _, name := range withAuthority { + typ := asserts.Type(name) + _, err := asserts.AssembleAndSignInTest(typ, nil, nil, testPrivKey1) + c.Check(err, ErrorMatches, `"authority-id" header is mandatory`) + } +} diff --git a/asserts/assertstest/assertstest.go b/asserts/assertstest/assertstest.go new file mode 100644 index 00000000..1fb4ad52 --- /dev/null +++ b/asserts/assertstest/assertstest.go @@ -0,0 +1,446 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +// Package assertstest provides helpers for testing code that involves assertions. +package assertstest + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "fmt" + "io" + "os/exec" + "strings" + "time" + + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/strutil" +) + +// GenerateKey generates a private/public key pair of the given bits. It panics on error. +func GenerateKey(bits int) (asserts.PrivateKey, *rsa.PrivateKey) { + priv, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + panic(fmt.Errorf("failed to create private key: %v", err)) + } + return asserts.RSAPrivateKey(priv), priv +} + +// ReadPrivKey reads a PGP private key (either armored or simply base64 encoded). It panics on error. +func ReadPrivKey(pk string) (asserts.PrivateKey, *rsa.PrivateKey) { + rd := bytes.NewReader([]byte(pk)) + blk, err := armor.Decode(rd) + var body io.Reader + if err == nil { + body = blk.Body + } else { + rd.Seek(0, 0) + // try unarmored + body = base64.NewDecoder(base64.StdEncoding, rd) + } + pkt, err := packet.Read(body) + if err != nil { + panic(err) + } + + pkPkt := pkt.(*packet.PrivateKey) + rsaPrivKey, ok := pkPkt.PrivateKey.(*rsa.PrivateKey) + if !ok { + panic("not a RSA key") + } + + return asserts.RSAPrivateKey(rsaPrivKey), rsaPrivKey +} + +// A sample developer key. +// See systestkeys for a prebuilt set of trusted keys and assertions. +const ( + DevKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQcYBFaFwYABEAC0kYiC4rsWFLJHEv/qO93LTMCAYKMLXFU0XN4XvqnkbwFc0QQd +lQcr7PwavYmKdWum+EmGWV/k5vZ0gwfZhBsL2MTWSNvO+5q5AYOqTq01CbSLcoN4 +cJI+BU348Vc/AoiIuuHro+gALs59HWsVSAKq7SNyHQfo257TKe8Q+Jjh095eruYJ +2kOvlAgAzjUv7eGDQ53O87wcwgZlCl0XqM/t+SRUxE5i8dQ4nySSekoTsWJo02kf +uMrWo3E5iEt6KKhfQtit2ZO91NYetIplzzZmaUOOkpziFTFW1NcwDKzDsLMh1EQ+ +ib+8mSWcou9m35aTkAQXlXlgqe5Pelj5+NUxnnoa1MR478Sv+guT+fbFQrl8PkMD +Jb/3PTKDPBNtjki5ZfIN9id4vidfBY4SCDftnj7yZMf5+1PPZ2XXHUoiUhHbGjST +F/23wr6OWvXe/AXX5BF4wJJTJxSxnYR6nleGMj4sbsbVsxIaqh1lMg5cuQjLr7eI +nxn994geUnQQsEPIVuVjLThJ/0sjXjy8kyxh6eieShZ6NZ0yLyIJRN5pnJ0ckRQF +T9Fs0UuMJZro0hR71t9mAuI45mSmznj78AvTvyuL+0aOj/lQa97NKbCsShYnKqsm +3Yzr03ahUMslwd6jZtRg+0ULYp9vaN7nwmsn6WWJ92CsCzFucdeJfJWKZQARAQAB +AA/9GSda3mzKRhms+hSt/MnJLFxtRpTvsZHztp8nOySO0ykZhf4B9kL/5EEXn3v+ +0IBp9jEJQQNrRd5cv79PFSB/igdw6C7vG+bV12bcGhnqrARFl9Vkdh8saCJiCcdI +8ZifP3jVJvfGxlu+3RP/ik/lOz1cnjVoGCqb9euWB4Wx+meCxyrTFdVHb4qOENqo +8xvOufPt5Fn0vwbSUDoA3N5h1NNLmdlc2BC7EQYuWI9biWHBBTxKHSanbv4GtE6F +wScvyVFtEM7J83xWNaHN07/pYpvQUuienSn5nRB6R5HEcWBIm/JPbWzP/mxRHoBe +HDUSa0z5HPXwGiSh84VmJrBgtlQosxk3jOHjynlU194S2cVLcSrFSf4hp6WZVAa1 +Nlkv6v62eU3nDxabkF92Lcv40s1cBqYCvhOtMzgoXL0TuaVJIdUwbJRHoBi8Bh5f +bNYJqyMqJNHcT9ylAWw130ljPTtqzbTMRtitxnJPbf60hpsJ4jcp2bJP9pg9XyuR +ZyIKtLfGQfxvFLsXzNssnVv7ZenK5AgUFTMvmyKCQQeYluheKc0KtRKSYE3iaVAs +Efw5Pd0GD82UGef9WahtnemodTlD3nkzlD50XBsd8xdNBQ7N2TFsP5Ldvfp1Wf2F +qg+rTaS0OID9vDQuekOcDI8lA9E4FYlIkJ6AqIb7hD5hlBMIAMRVXLlPLgzmrY5k +pIPMbgyN0wm3f4qAWIaMtg79x9gTylsGF7lkqNLqFDFYfoUHb+iXINYc51kHV7Ka +JifHhdy8TaBTBrIrsFLJpv06lRex/fdyvswev3W1g3wRJ86eTCqwr1DjB+q2kYX8 +u1qDPFRzK4WF+wOF/FwCBLDpESmHSapXuzL5i6pJfOCFIJqT/Q/yp9tyTcxs82tu +kSlNKoXrZi4xHsDpPBuNjMl3eIz3ogesUG60MMa6xovVGV3ICJcwYwycvvQcjuxS +XtJlHK+/G3kB87BXzNCMyUGfDNy7mcTrXAXoUH8nCu4ipyaT/jEyvi95w/7RJcFU +qs6taH8IAOtxqnBZGDQuYPF0ZmZQ7e1/FXq/LBQryYZgNtyLUdR7ycXGCTXlEIGw +X3r7Qf4+a3MlriP5thKxci+npcIj4e31aS6cpO2IcGJzmNOHzLCl3b4XmO/APBSA +FZpQE3k+lg45tn/vgcPMKKLAAv6TbpVVgLrFXGtX3Gtkd66fPPOcINXi6+MqXfp5 +rl8OJIq5O5ygbbglwcqmeI29RLZ58b0ktCa5ZZNzeSV+T5jHwRNnWm0EJgjx8Lwn +LEWFS/vQjGwaoRJi06jpmM+66sefyTQ3qvyzQLBqlenZf16GGz28cOSjNJ9FDth1 +iKnyk7d8nqhmbSHeW08QUwTF6NGp+xsIAJDa3ouxSjTEB1P7z1VLJp6nSglBQ74n +XAprk2WpggLNrWwyFJsgFh07LxShb/O3t1TanU+Ld/ryyWHztTxag2auAHuVQ4+S +EkjKqkUaSOQML9a9AvZ2rQr84f5ohc/vCOQhpNVLSyw55EW6WhnntNWVwgZxMiAj +oREMJMrBb6LL9b7kHtfYqLNfe3fkUx+tuTsm96Wi1cdkh0qyut0+J+eieZVn7kiM +UP5IZuz9TSjDOrA5qu5NGlbXNaN0cdJ2UUSNekQiysqDpdf00wIwr1XqH+KLUjZv +pO5Mub6NdnVXJRZunpbNXbuxj49NXnZEEi71WQm9KLR8KQ1oQ+RlnHx/XLQHICh0 +ZXN0KYkCOAQTAQIAIgUCVoXBgAIbLwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA +CgkQSkI9KKrqS0/YEhAAgJALHrx4kFRcgDJE+khK/CdoaLvi0N40eAE+RzQgcxhh +S4Aeks8n1cL6oAwDfCL+ohyWvPzF2DzsBkEIC3l+JS2tn0JJ+qexY+qhdGkEze/o +SIvH9sfR5LJuKb3OAt2mQlY+sxjlkzU9rTGKsVZxgApNM4665dlagF9tipMQTHnd +eFZRlvNTWKkweW0jbJCpRKlQnjEZ6S/wlPBgH69Ek3bnDcgp6eaAU92Ke9Fa2wMV +LBMaXpUIvddKFjoGtvShDOpcQRE99Z8tK4YSAOg+zbSUeD7HGH00EQERItoJsAv1 +7Du8+jcKSeOhz7PPxOA7mEnYNdoMcrg/2AP+FVI6zGYcKN7Hq3C6Z+bQ4X1VkKmv +NCFomU2AyPVxpJRYw7/EkoRWp/iq6sEb7bsmhmDEiz1MiroAV+efmWyUjxueSzrW +24OxHTWi2GuHBF+FKUD3UxfaWMjH+tuWYPIHzYsT+TfsN0vAEFyhRi8Ncelu1RV4 +x2O3wmjxoaX/2FmyuU5WhcVkcpRFgceyf1/86NP9gT5MKbWtJC85YYpxibnvPdGd ++sqtEEqgX3dSsHT+rkBk7kf3ghDwsLtnliFPOeAaIHGZl754EpK+qPUTnYZK022H +2crhYlApO9+06kBeybSO6joMUR007883I9GELYhzmuEjpVGquJQ3+S5QtW1to0w= +=5Myf +-----END PGP PRIVATE KEY BLOCK----- +` + + DevKeyID = "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu" + + DevKeyPGPFingerprint = "966e70f4b9f257a2772f8f354a423d28aaea4b4f" +) + +// GPGImportKey imports the given PGP armored key into the GnuPG setup at homedir. It panics on error. +func GPGImportKey(homedir, armoredKey string) { + path, err := exec.LookPath("gpg1") + if err != nil { + path, err = exec.LookPath("gpg") + } + if err != nil { + panic(err) + } + gpg := exec.Command(path, "--homedir", homedir, "-q", "--batch", "--import", "--armor") + gpg.Stdin = bytes.NewBufferString(armoredKey) + out, err := gpg.CombinedOutput() + if err != nil { + panic(fmt.Errorf("cannot import test key into GPG setup at %q: %v (%q)", homedir, err, out)) + } +} + +// A SignerDB can sign assertions using its key pairs. +type SignerDB interface { + Sign(assertType *asserts.AssertionType, headers map[string]interface{}, body []byte, keyID string) (asserts.Assertion, error) +} + +// NewAccount creates an account assertion for username, it fills in values for other missing headers as needed. It panics on error. +func NewAccount(db SignerDB, username string, otherHeaders map[string]interface{}, keyID string) *asserts.Account { + if otherHeaders == nil { + otherHeaders = make(map[string]interface{}) + } + otherHeaders["username"] = username + if otherHeaders["account-id"] == nil { + otherHeaders["account-id"] = strutil.MakeRandomString(32) + } + if otherHeaders["display-name"] == nil { + otherHeaders["display-name"] = strings.ToTitle(username[:1]) + username[1:] + } + if otherHeaders["validation"] == nil { + otherHeaders["validation"] = "unproven" + } + if otherHeaders["timestamp"] == nil { + otherHeaders["timestamp"] = time.Now().Format(time.RFC3339) + } + a, err := db.Sign(asserts.AccountType, otherHeaders, nil, keyID) + if err != nil { + panic(err) + } + return a.(*asserts.Account) +} + +// NewAccountKey creates an account-key assertion for the account, it fills in values for missing headers as needed. In panics on error. +func NewAccountKey(db SignerDB, acct *asserts.Account, otherHeaders map[string]interface{}, pubKey asserts.PublicKey, keyID string) *asserts.AccountKey { + if otherHeaders == nil { + otherHeaders = make(map[string]interface{}) + } + otherHeaders["account-id"] = acct.AccountID() + otherHeaders["public-key-sha3-384"] = pubKey.ID() + if otherHeaders["name"] == nil { + otherHeaders["name"] = "default" + } + if otherHeaders["since"] == nil { + otherHeaders["since"] = time.Now().Format(time.RFC3339) + } + encodedPubKey, err := asserts.EncodePublicKey(pubKey) + if err != nil { + panic(err) + } + a, err := db.Sign(asserts.AccountKeyType, otherHeaders, encodedPubKey, keyID) + if err != nil { + panic(err) + } + return a.(*asserts.AccountKey) +} + +// SigningDB embeds a signing assertion database with a default private key and assigned authority id. +// Sign will use the assigned authority id. +// "" can be passed for keyID to Sign and PublicKey to use the default key. +type SigningDB struct { + AuthorityID string + KeyID string + + *asserts.Database +} + +// NewSigningDB creates a test signing assertion db with the given defaults. It panics on error. +func NewSigningDB(authorityID string, privKey asserts.PrivateKey) *SigningDB { + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{}) + if err != nil { + panic(err) + } + err = db.ImportKey(privKey) + if err != nil { + panic(err) + } + return &SigningDB{ + AuthorityID: authorityID, + KeyID: privKey.PublicKey().ID(), + Database: db, + } +} + +func (db *SigningDB) Sign(assertType *asserts.AssertionType, headers map[string]interface{}, body []byte, keyID string) (asserts.Assertion, error) { + if _, ok := headers["authority-id"]; !ok { + // copy before modifying + headers2 := make(map[string]interface{}, len(headers)+1) + for h, v := range headers { + headers2[h] = v + } + headers = headers2 + headers["authority-id"] = db.AuthorityID + } + if keyID == "" { + keyID = db.KeyID + } + return db.Database.Sign(assertType, headers, body, keyID) +} + +func (db *SigningDB) PublicKey(keyID string) (asserts.PublicKey, error) { + if keyID == "" { + keyID = db.KeyID + } + return db.Database.PublicKey(keyID) +} + +// StoreStack realises a store-like set of founding trusted assertions and signing setup. +type StoreStack struct { + // Trusted authority assertions. + TrustedAccount *asserts.Account + TrustedKey *asserts.AccountKey + Trusted []asserts.Assertion + + // Generic authority assertions. + GenericAccount *asserts.Account + GenericKey *asserts.AccountKey + GenericModelsKey *asserts.AccountKey + Generic []asserts.Assertion + GenericClassicModel *asserts.Model + + // Signing assertion db that signs with the root private key. + RootSigning *SigningDB + + // The store-like signing functionality that signs with a store key, setup to also store assertions if desired. It stores a default account-key for the store private key, see also the StoreStack.Key method. + *SigningDB +} + +// StoreKeys holds a set of store private keys. +type StoreKeys struct { + Root asserts.PrivateKey + Store asserts.PrivateKey + Generic asserts.PrivateKey + GenericModels asserts.PrivateKey +} + +var ( + rootPrivKey, _ = GenerateKey(1024) + storePrivKey, _ = GenerateKey(752) + genericPrivKey, _ = GenerateKey(752) + genericModelsPrivKey, _ = GenerateKey(752) + + pregenKeys = StoreKeys{ + Root: rootPrivKey, + Store: storePrivKey, + Generic: genericPrivKey, + GenericModels: genericModelsPrivKey, + } +) + +// NewStoreStack creates a new store assertion stack. It panics on error. +// Optional keys specify private keys to use for the various roles. +func NewStoreStack(authorityID string, keys *StoreKeys) *StoreStack { + if keys == nil { + keys = &pregenKeys + } + + rootSigning := NewSigningDB(authorityID, keys.Root) + ts := time.Now().Format(time.RFC3339) + trustedAcct := NewAccount(rootSigning, authorityID, map[string]interface{}{ + "account-id": authorityID, + "validation": "certified", + "timestamp": ts, + }, "") + trustedKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{ + "name": "root", + "since": ts, + }, keys.Root.PublicKey(), "") + trusted := []asserts.Assertion{trustedAcct, trustedKey} + + genericAcct := NewAccount(rootSigning, "generic", map[string]interface{}{ + "account-id": "generic", + "validation": "certified", + "timestamp": ts, + }, "") + + err := rootSigning.ImportKey(keys.GenericModels) + if err != nil { + panic(err) + } + genericModelsKey := NewAccountKey(rootSigning, genericAcct, map[string]interface{}{ + "name": "models", + "since": ts, + }, keys.GenericModels.PublicKey(), "") + generic := []asserts.Assertion{genericAcct, genericModelsKey} + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: trusted, + OtherPredefined: generic, + }) + if err != nil { + panic(err) + } + err = db.ImportKey(keys.Store) + if err != nil { + panic(err) + } + storeKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{ + "name": "store", + }, keys.Store.PublicKey(), "") + err = db.Add(storeKey) + if err != nil { + panic(err) + } + + err = db.ImportKey(keys.Generic) + if err != nil { + panic(err) + } + genericKey := NewAccountKey(rootSigning, genericAcct, map[string]interface{}{ + "name": "serials", + "since": ts, + }, keys.Generic.PublicKey(), "") + err = db.Add(genericKey) + if err != nil { + panic(err) + } + + a, err := rootSigning.Sign(asserts.ModelType, map[string]interface{}{ + "authority-id": "generic", + "series": "16", + "brand-id": "generic", + "model": "generic-classic", + "classic": "true", + "timestamp": ts, + }, nil, genericModelsKey.PublicKeyID()) + if err != nil { + panic(err) + } + genericClassicMod := a.(*asserts.Model) + + return &StoreStack{ + TrustedAccount: trustedAcct, + TrustedKey: trustedKey, + Trusted: trusted, + + GenericAccount: genericAcct, + GenericKey: genericKey, + GenericModelsKey: genericModelsKey, + Generic: generic, + GenericClassicModel: genericClassicMod, + + RootSigning: rootSigning, + + SigningDB: &SigningDB{ + AuthorityID: authorityID, + KeyID: storeKey.PublicKeyID(), + Database: db, + }, + } +} + +// StoreAccountKey retrieves one of the account-key assertions for the signing keys of the simulated store signing database. +// "" for keyID means the default one. It panics on error. +func (ss *StoreStack) StoreAccountKey(keyID string) *asserts.AccountKey { + if keyID == "" { + keyID = ss.KeyID + } + key, err := ss.Find(asserts.AccountKeyType, map[string]string{ + "account-id": ss.AuthorityID, + "public-key-sha3-384": keyID, + }) + if asserts.IsNotFound(err) { + return nil + } + if err != nil { + panic(err) + } + return key.(*asserts.AccountKey) +} + +// MockBuiltinBaseDeclaration mocks the builtin base-declaration exposed by asserts.BuiltinBaseDeclaration. +func MockBuiltinBaseDeclaration(headers []byte) (restore func()) { + var prevHeaders []byte + decl := asserts.BuiltinBaseDeclaration() + if decl != nil { + prevHeaders, _ = decl.Signature() + } + + err := asserts.InitBuiltinBaseDeclaration(headers) + if err != nil { + panic(err) + } + + return func() { + err := asserts.InitBuiltinBaseDeclaration(prevHeaders) + if err != nil { + panic(err) + } + } +} diff --git a/asserts/assertstest/assertstest_test.go b/asserts/assertstest/assertstest_test.go new file mode 100644 index 00000000..8bfb3c1a --- /dev/null +++ b/asserts/assertstest/assertstest_test.go @@ -0,0 +1,163 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package assertstest_test + +import ( + "encoding/hex" + "testing" + "time" + + "golang.org/x/crypto/openpgp/packet" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +func TestAssertsTest(t *testing.T) { TestingT(t) } + +type helperSuite struct{} + +var _ = Suite(&helperSuite{}) + +func (s *helperSuite) TestReadPrivKeyArmored(c *C) { + pk, rsaPrivKey := assertstest.ReadPrivKey(assertstest.DevKey) + c.Check(pk, NotNil) + c.Check(rsaPrivKey, NotNil) + c.Check(pk.PublicKey().ID(), Equals, assertstest.DevKeyID) + pkt := packet.NewRSAPrivateKey(time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC), rsaPrivKey) + c.Check(hex.EncodeToString(pkt.Fingerprint[:]), Equals, assertstest.DevKeyPGPFingerprint) +} + +const ( + base64PrivKey = ` +xcLYBFaU5cgBCAC/2wUYK7YzvL6f0ZxBfptFVfNmI7G9J9Eszdoq1NZZXaV+aYeC7eNU +1sKdO6wIRcw3lvybtq5W1n4D/jJAb2qXbB6BukuCGVXCLMEUdvheaVVcIZ/LwdbxmgMJsDFoHsDC +RzjkUVTU2b8sK6MwANIsSS5r8Lwm7FazD1qq50UdebsIx8dkjFR5VwrCYgOu1MO2Bqka7UU9as2q +4ZsFzpcS/so41kd4IPFEmNMlejhSjgCaixehpLeXypQVHLluV+oSPMV7GtE7Z6HO4V5cT2c9RdXg +l4jSKY91rHInkmSizF03laL3T/I6oj0FdZG9GB6QzqRCBTzK05cnVP1k7WFJABEBAAEAB/9spiIa +cBa88fSaGWB+Dq7r8yLmAuzTDEt/LgyRGPtSnJ/uGOEvGn0VPJH17ScdgDmIea8Ql8HfV5UBueDH +cNFSc15LZS8BvEs+rY2ig0VgYhJ/HGOcRmftZqS1xdwU9OWAoEjts8lwyOdkoknGE5Dyl3b8ldZX +zJvEx7s28cXITH4UwGEAMHEXrAMCjkcKPVbM7vW81uOWn0U1jMzmfmqrcLkSfvaCnep6+4QphKPy +B4DxJAI34EvJAru4iL5bWWvMeXkBZgmBy4g2SlYbk09cfTmhzw6di5GZtg+77yGACltPBA8MSbzF +v30apQ5iuI/hVin7U2/QtQHP4d0zUDbpBADusynnaFcDnPEUm4RdvNpujaBC/HfIpOstiS36RZy8 +lZeVtffa/+DqzodZD9YF7zEVWeUiC5Os4THirYOZ04dM5yqR/GlKXMHGHaT+mnhD8g1hORx/LrMO +k5wUpD1NmloSjP/0pJRccuXq7O1QQfls1Hq1vOSh3cZ/aIvTONJ/YwQAzcK0/2SrnaUc3oCxMEuI +2FX0LsYDQiXzMK/x/lfZ/ywxt5J/q6CuaG3xXgSHlsk0M8Uo4acZqpCIFA9mwCPxKbrIOGnwJsI/ ++sZBkngtZMSS88Vl32gnzpVWLGpbW2F7hnWrj1YigTcFUdi6TFNa7zHPASzCKxKKiz9YxEWWymME +AIbURnQJJOSfYgFyloQuA2QWyAK5Zu7qPworBoRo+PZPVb5yQmSUQ21VqNfzqIJz1EgiDZ0NyGid +uXAjn58O9tAq7IN5pTeHoTacZ75cI82kQkUxEnfiKjBO/AU30Y3COsIXhtbIXbtcitHSicp4lnpU +NejDkxUnC2wIvJzHWo1FQ18= +` +) + +func (s *helperSuite) TestReadPrivKeyUnarmored(c *C) { + pk, rsaPrivKey := assertstest.ReadPrivKey(base64PrivKey) + c.Check(pk, NotNil) + c.Check(rsaPrivKey, NotNil) +} + +func (s *helperSuite) TestStoreStack(c *C) { + store := assertstest.NewStoreStack("super", nil) + + c.Check(store.TrustedAccount.AccountID(), Equals, "super") + c.Check(store.TrustedAccount.IsCertified(), Equals, true) + + c.Check(store.TrustedKey.AccountID(), Equals, "super") + c.Check(store.TrustedKey.Name(), Equals, "root") + + c.Check(store.GenericAccount.AccountID(), Equals, "generic") + c.Check(store.GenericAccount.IsCertified(), Equals, true) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + OtherPredefined: store.Generic, + }) + c.Assert(err, IsNil) + + storeAccKey := store.StoreAccountKey("") + c.Assert(storeAccKey, NotNil) + + c.Check(storeAccKey.AccountID(), Equals, "super") + c.Check(storeAccKey.AccountID(), Equals, store.AuthorityID) + c.Check(storeAccKey.PublicKeyID(), Equals, store.KeyID) + c.Check(storeAccKey.Name(), Equals, "store") + + c.Check(store.GenericKey.AccountID(), Equals, "generic") + c.Check(store.GenericKey.Name(), Equals, "serials") + + c.Check(store.GenericModelsKey.AccountID(), Equals, "generic") + c.Check(store.GenericModelsKey.Name(), Equals, "models") + + g, err := store.Find(asserts.AccountType, map[string]string{ + "account-id": "generic", + }) + c.Assert(err, IsNil) + c.Assert(g.Headers(), DeepEquals, store.GenericAccount.Headers()) + + g, err = store.Find(asserts.AccountKeyType, map[string]string{ + "public-key-sha3-384": store.GenericKey.PublicKeyID(), + }) + c.Assert(err, IsNil) + c.Assert(g.Headers(), DeepEquals, store.GenericKey.Headers()) + + g, err = store.Find(asserts.AccountKeyType, map[string]string{ + "public-key-sha3-384": store.GenericModelsKey.PublicKeyID(), + }) + c.Assert(err, IsNil) + c.Assert(g.Headers(), DeepEquals, store.GenericModelsKey.Headers()) + + acct := assertstest.NewAccount(store, "devel1", nil, "") + c.Check(acct.Username(), Equals, "devel1") + c.Check(acct.AccountID(), HasLen, 32) + c.Check(acct.IsCertified(), Equals, false) + + err = db.Add(storeAccKey) + c.Assert(err, IsNil) + + err = db.Add(acct) + c.Assert(err, IsNil) + + devKey, _ := assertstest.GenerateKey(752) + + acctKey := assertstest.NewAccountKey(store, acct, nil, devKey.PublicKey(), "") + + err = db.Add(acctKey) + c.Assert(err, IsNil) + + c.Check(acctKey.Name(), Equals, "default") + + a, err := db.Find(asserts.AccountType, map[string]string{ + "account-id": "generic", + }) + c.Assert(err, IsNil) + c.Assert(a.Headers(), DeepEquals, store.GenericAccount.Headers()) + + c.Check(store.GenericClassicModel.AuthorityID(), Equals, "generic") + c.Check(store.GenericClassicModel.BrandID(), Equals, "generic") + c.Check(store.GenericClassicModel.Model(), Equals, "generic-classic") + c.Check(store.GenericClassicModel.Classic(), Equals, true) + err = db.Check(store.GenericClassicModel) + c.Assert(err, IsNil) + + err = db.Add(store.GenericKey) + c.Assert(err, IsNil) +} diff --git a/asserts/crypto.go b/asserts/crypto.go new file mode 100644 index 00000000..d9397be4 --- /dev/null +++ b/asserts/crypto.go @@ -0,0 +1,398 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + _ "crypto/sha256" // be explicit about supporting SHA256 + _ "crypto/sha512" // be explicit about needing SHA512 + "encoding/base64" + "fmt" + "io" + "time" + + "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/sha3" +) + +const ( + maxEncodeLineLength = 76 + v1 = 0x1 +) + +var ( + v1Header = []byte{v1} + v1FixedTimestamp = time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) +) + +func encodeV1(data []byte) []byte { + buf := new(bytes.Buffer) + buf.Grow(base64.StdEncoding.EncodedLen(len(data) + 1)) + enc := base64.NewEncoder(base64.StdEncoding, buf) + enc.Write(v1Header) + enc.Write(data) + enc.Close() + flat := buf.Bytes() + flatSize := len(flat) + + buf = new(bytes.Buffer) + buf.Grow(flatSize + flatSize/maxEncodeLineLength + 1) + off := 0 + for { + endOff := off + maxEncodeLineLength + if endOff > flatSize { + endOff = flatSize + } + buf.Write(flat[off:endOff]) + off = endOff + if off >= flatSize { + break + } + buf.WriteByte('\n') + } + + return buf.Bytes() +} + +type keyEncoder interface { + keyEncode(w io.Writer) error +} + +func encodeKey(key keyEncoder, kind string) ([]byte, error) { + buf := new(bytes.Buffer) + err := key.keyEncode(buf) + if err != nil { + return nil, fmt.Errorf("cannot encode %s: %v", kind, err) + } + return encodeV1(buf.Bytes()), nil +} + +type openpgpSigner interface { + sign(content []byte) (*packet.Signature, error) +} + +func signContent(content []byte, privateKey PrivateKey) ([]byte, error) { + signer, ok := privateKey.(openpgpSigner) + if !ok { + panic(fmt.Errorf("not an internally supported PrivateKey: %T", privateKey)) + } + + sig, err := signer.sign(content) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + err = sig.Serialize(buf) + if err != nil { + return nil, err + } + + return encodeV1(buf.Bytes()), nil +} + +func decodeV1(b []byte, kind string) (packet.Packet, error) { + if len(b) == 0 { + return nil, fmt.Errorf("cannot decode %s: no data", kind) + } + buf := make([]byte, base64.StdEncoding.DecodedLen(len(b))) + n, err := base64.StdEncoding.Decode(buf, b) + if err != nil { + return nil, fmt.Errorf("cannot decode %s: %v", kind, err) + } + if n == 0 { + return nil, fmt.Errorf("cannot decode %s: base64 without data", kind) + } + buf = buf[:n] + if buf[0] != v1 { + return nil, fmt.Errorf("unsupported %s format version: %d", kind, buf[0]) + } + rd := bytes.NewReader(buf[1:]) + pkt, err := packet.Read(rd) + if err != nil { + return nil, fmt.Errorf("cannot decode %s: %v", kind, err) + } + if rd.Len() != 0 { + return nil, fmt.Errorf("%s has spurious trailing data", kind) + } + return pkt, nil +} + +func decodeSignature(signature []byte) (*packet.Signature, error) { + pkt, err := decodeV1(signature, "signature") + if err != nil { + return nil, err + } + sig, ok := pkt.(*packet.Signature) + if !ok { + return nil, fmt.Errorf("expected signature, got instead: %T", pkt) + } + return sig, nil +} + +// PublicKey is the public part of a cryptographic private/public key pair. +type PublicKey interface { + // ID returns the id of the key used for lookup. + ID() string + + // verify verifies signature is valid for content using the key. + verify(content []byte, sig *packet.Signature) error + + keyEncoder +} + +type openpgpPubKey struct { + pubKey *packet.PublicKey + sha3_384 string +} + +func (opgPubKey *openpgpPubKey) ID() string { + return opgPubKey.sha3_384 +} + +func (opgPubKey *openpgpPubKey) verify(content []byte, sig *packet.Signature) error { + h := sig.Hash.New() + h.Write(content) + return opgPubKey.pubKey.VerifySignature(h, sig) +} + +func (opgPubKey openpgpPubKey) keyEncode(w io.Writer) error { + return opgPubKey.pubKey.Serialize(w) +} + +func newOpenPGPPubKey(intPubKey *packet.PublicKey) *openpgpPubKey { + h := sha3.New384() + h.Write(v1Header) + err := intPubKey.Serialize(h) + if err != nil { + panic("internal error: cannot compute public key sha3-384") + } + sha3_384, err := EncodeDigest(crypto.SHA3_384, h.Sum(nil)) + if err != nil { + panic("internal error: cannot compute public key sha3-384") + } + return &openpgpPubKey{pubKey: intPubKey, sha3_384: sha3_384} +} + +// RSAPublicKey returns a database useable public key out of rsa.PublicKey. +func RSAPublicKey(pubKey *rsa.PublicKey) PublicKey { + intPubKey := packet.NewRSAPublicKey(v1FixedTimestamp, pubKey) + return newOpenPGPPubKey(intPubKey) +} + +// DecodePublicKey deserializes a public key. +func DecodePublicKey(pubKey []byte) (PublicKey, error) { + pkt, err := decodeV1(pubKey, "public key") + if err != nil { + return nil, err + } + pubk, ok := pkt.(*packet.PublicKey) + if !ok { + return nil, fmt.Errorf("expected public key, got instead: %T", pkt) + } + rsaPubKey, ok := pubk.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("expected RSA public key, got instead: %T", pubk.PublicKey) + } + return RSAPublicKey(rsaPubKey), nil +} + +// EncodePublicKey serializes a public key, typically for embedding in an assertion. +func EncodePublicKey(pubKey PublicKey) ([]byte, error) { + return encodeKey(pubKey, "public key") +} + +// PrivateKey is a cryptographic private/public key pair. +type PrivateKey interface { + // PublicKey returns the public part of the pair. + PublicKey() PublicKey + + keyEncoder +} + +type openpgpPrivateKey struct { + privk *packet.PrivateKey +} + +func (opgPrivK openpgpPrivateKey) PublicKey() PublicKey { + return newOpenPGPPubKey(&opgPrivK.privk.PublicKey) +} + +func (opgPrivK openpgpPrivateKey) keyEncode(w io.Writer) error { + return opgPrivK.privk.Serialize(w) +} + +var openpgpConfig = &packet.Config{ + DefaultHash: crypto.SHA512, +} + +func (opgPrivK openpgpPrivateKey) sign(content []byte) (*packet.Signature, error) { + privk := opgPrivK.privk + sig := new(packet.Signature) + sig.PubKeyAlgo = privk.PubKeyAlgo + sig.Hash = openpgpConfig.Hash() + sig.CreationTime = time.Now() + + h := openpgpConfig.Hash().New() + h.Write(content) + + err := sig.Sign(h, privk, openpgpConfig) + if err != nil { + return nil, err + } + + return sig, nil +} + +func decodePrivateKey(privKey []byte) (PrivateKey, error) { + pkt, err := decodeV1(privKey, "private key") + if err != nil { + return nil, err + } + privk, ok := pkt.(*packet.PrivateKey) + if !ok { + return nil, fmt.Errorf("expected private key, got instead: %T", pkt) + } + if _, ok := privk.PrivateKey.(*rsa.PrivateKey); !ok { + return nil, fmt.Errorf("expected RSA private key, got instead: %T", privk.PrivateKey) + } + return openpgpPrivateKey{privk}, nil +} + +// RSAPrivateKey returns a PrivateKey for database use out of a rsa.PrivateKey. +func RSAPrivateKey(privk *rsa.PrivateKey) PrivateKey { + intPrivk := packet.NewRSAPrivateKey(v1FixedTimestamp, privk) + return openpgpPrivateKey{intPrivk} +} + +// GenerateKey generates a private/public key pair. +func GenerateKey() (PrivateKey, error) { + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + return RSAPrivateKey(priv), nil +} + +func encodePrivateKey(privKey PrivateKey) ([]byte, error) { + return encodeKey(privKey, "private key") +} + +// externally held key pairs + +type extPGPPrivateKey struct { + pubKey PublicKey + from string + pgpFingerprint string + bitLen int + doSign func(content []byte) ([]byte, error) +} + +func newExtPGPPrivateKey(exportedPubKeyStream io.Reader, from string, sign func(content []byte) ([]byte, error)) (*extPGPPrivateKey, error) { + var pubKey *packet.PublicKey + + rd := packet.NewReader(exportedPubKeyStream) + for { + pkt, err := rd.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("cannot read exported public key: %v", err) + } + cand, ok := pkt.(*packet.PublicKey) + if ok { + if cand.IsSubkey { + continue + } + if pubKey != nil { + return nil, fmt.Errorf("cannot select exported public key, found many") + } + pubKey = cand + } + } + + if pubKey == nil { + return nil, fmt.Errorf("cannot read exported public key, found none (broken export)") + + } + + rsaPubKey, ok := pubKey.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("not a RSA key") + } + + return &extPGPPrivateKey{ + pubKey: RSAPublicKey(rsaPubKey), + from: from, + pgpFingerprint: fmt.Sprintf("%X", pubKey.Fingerprint), + bitLen: rsaPubKey.N.BitLen(), + doSign: sign, + }, nil +} + +func (expk *extPGPPrivateKey) fingerprint() string { + return expk.pgpFingerprint +} + +func (expk *extPGPPrivateKey) PublicKey() PublicKey { + return expk.pubKey +} + +func (expk *extPGPPrivateKey) keyEncode(w io.Writer) error { + return fmt.Errorf("cannot access external private key to encode it") +} + +func (expk *extPGPPrivateKey) sign(content []byte) (*packet.Signature, error) { + if expk.bitLen < 4096 { + return nil, fmt.Errorf("signing needs at least a 4096 bits key, got %d", expk.bitLen) + } + + out, err := expk.doSign(content) + if err != nil { + return nil, err + } + + badSig := fmt.Sprintf("bad %s produced signature: ", expk.from) + + sigpkt, err := packet.Read(bytes.NewBuffer(out)) + if err != nil { + return nil, fmt.Errorf(badSig+"%v", err) + } + + sig, ok := sigpkt.(*packet.Signature) + if !ok { + return nil, fmt.Errorf(badSig+"got %T", sigpkt) + } + + if sig.Hash != crypto.SHA512 { + return nil, fmt.Errorf(badSig + "expected SHA512 digest") + } + + err = expk.pubKey.verify(content, sig) + if err != nil { + return nil, fmt.Errorf(badSig+"it does not verify: %v", err) + } + + return sig, nil +} diff --git a/asserts/database.go b/asserts/database.go new file mode 100644 index 00000000..14a4018d --- /dev/null +++ b/asserts/database.go @@ -0,0 +1,623 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +// Package asserts implements snappy assertions and a database +// abstraction for managing and holding them. +package asserts + +import ( + "fmt" + "regexp" + "time" +) + +// NotFoundError is returned when an assertion can not be found. +type NotFoundError struct { + Type *AssertionType + Headers map[string]string +} + +func (e *NotFoundError) Error() string { + pk, err := PrimaryKeyFromHeaders(e.Type, e.Headers) + if err != nil || len(e.Headers) != len(pk) { + // TODO: worth conveying more information? + return fmt.Sprintf("%s assertion not found", e.Type.Name) + } + + return fmt.Sprintf("%v not found", &Ref{Type: e.Type, PrimaryKey: pk}) +} + +// IsNotFound returns whether err is an assertion not found error. +func IsNotFound(err error) bool { + _, ok := err.(*NotFoundError) + return ok +} + +// A Backstore stores assertions. It can store and retrieve assertions +// by type under unique primary key headers (whose names are available +// from assertType.PrimaryKey). Plus it supports searching by headers. +// Lookups can be limited to a maximum allowed format. +type Backstore interface { + // Put stores an assertion. + // It is responsible for checking that assert is newer than a + // previously stored revision with the same primary key headers. + Put(assertType *AssertionType, assert Assertion) error + // Get returns the assertion with the given unique key for its + // primary key headers. If none is present it returns a + // NotFoundError, usually with omitted Headers. + Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) + // Search returns assertions matching the given headers. + // It invokes foundCb for each found assertion. + Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error +} + +type nullBackstore struct{} + +func (nbs nullBackstore) Put(t *AssertionType, a Assertion) error { + return fmt.Errorf("cannot store assertions without setting a proper assertion backstore implementation") +} + +func (nbs nullBackstore) Get(t *AssertionType, k []string, maxFormat int) (Assertion, error) { + return nil, &NotFoundError{Type: t} +} + +func (nbs nullBackstore) Search(t *AssertionType, h map[string]string, f func(Assertion), maxFormat int) error { + return nil +} + +// A KeypairManager is a manager and backstore for private/public key pairs. +type KeypairManager interface { + // Put stores the given private/public key pair, + // making sure it can be later retrieved by its unique key id with Get. + // Trying to store a key with an already present key id should + // result in an error. + Put(privKey PrivateKey) error + // Get returns the private/public key pair with the given key id. + Get(keyID string) (PrivateKey, error) +} + +// DatabaseConfig for an assertion database. +type DatabaseConfig struct { + // trusted set of assertions (account and account-key supported), + // used to establish root keys and trusted authorities + Trusted []Assertion + // predefined assertions but that do not establish foundational trust + OtherPredefined []Assertion + // backstore for assertions, left unset storing assertions will error + Backstore Backstore + // manager/backstore for keypairs, defaults to in-memory implementation + KeypairManager KeypairManager + // assertion checkers used by Database.Check, left unset DefaultCheckers will be used which is recommended + Checkers []Checker +} + +// RevisionError indicates a revision improperly used for an operation. +type RevisionError struct { + Used, Current int +} + +func (e *RevisionError) Error() string { + if e.Used < 0 || e.Current < 0 { + // TODO: message may need tweaking once there's a use. + return fmt.Sprintf("assertion revision is unknown") + } + if e.Used == e.Current { + return fmt.Sprintf("revision %d is already the current revision", e.Used) + } + if e.Used < e.Current { + return fmt.Sprintf("revision %d is older than current revision %d", e.Used, e.Current) + } + return fmt.Sprintf("revision %d is more recent than current revision %d", e.Used, e.Current) +} + +// UnsupportedFormatError indicates an assertion with a format iteration not yet supported by the present version of asserts. +type UnsupportedFormatError struct { + Ref *Ref + Format int + // Update marks there was already a current revision of the assertion and it has been kept. + Update bool +} + +func (e *UnsupportedFormatError) Error() string { + postfx := "" + if e.Update { + postfx = " (current not updated)" + } + return fmt.Sprintf("proposed %q assertion has format %d but %d is latest supported%s", e.Ref.Type.Name, e.Format, e.Ref.Type.MaxSupportedFormat(), postfx) +} + +// IsUnaccceptedUpdate returns whether the error indicates that an +// assertion revision was already present and has been kept because +// the update was not accepted. +func IsUnaccceptedUpdate(err error) bool { + switch x := err.(type) { + case *UnsupportedFormatError: + return x.Update + case *RevisionError: + return x.Used <= x.Current + } + return false +} + +// A RODatabase exposes read-only access to an assertion database. +type RODatabase interface { + // IsTrustedAccount returns whether the account is part of the trusted set. + IsTrustedAccount(accountID string) bool + // Find an assertion based on arbitrary headers. + // Provided headers must contain the primary key for the assertion type. + // It returns a NotFoundError if the assertion cannot be found. + Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) + // FindPredefined finds an assertion in the predefined sets + // (trusted or not) based on arbitrary headers. Provided + // headers must contain the primary key for the assertion + // type. It returns a NotFoundError if the assertion cannot + // be found. + FindPredefined(assertionType *AssertionType, headers map[string]string) (Assertion, error) + // FindTrusted finds an assertion in the trusted set based on + // arbitrary headers. Provided headers must contain the + // primary key for the assertion type. It returns a + // NotFoundError if the assertion cannot be found. + FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) + // FindMany finds assertions based on arbitrary headers. + // It returns a NotFoundError if no assertion can be found. + FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) + // FindManyPredefined finds assertions in the predefined sets + // (trusted or not) based on arbitrary headers. It returns a + // NotFoundError if no assertion can be found. + FindManyPredefined(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) + // Check tests whether the assertion is properly signed and consistent with all the stored knowledge. + Check(assert Assertion) error +} + +// A Checker defines a check on an assertion considering aspects such as +// the signing key, and consistency with other +// assertions in the database. +type Checker func(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error + +// Database holds assertions and can be used to sign or check +// further assertions. +type Database struct { + bs Backstore + keypairMgr KeypairManager + trusted Backstore + predefined Backstore + backstores []Backstore + checkers []Checker +} + +// OpenDatabase opens the assertion database based on the configuration. +func OpenDatabase(cfg *DatabaseConfig) (*Database, error) { + bs := cfg.Backstore + keypairMgr := cfg.KeypairManager + + if bs == nil { + bs = nullBackstore{} + } + if keypairMgr == nil { + keypairMgr = NewMemoryKeypairManager() + } + + trustedBackstore := NewMemoryBackstore() + + for _, a := range cfg.Trusted { + switch accepted := a.(type) { + case *AccountKey: + accKey := accepted + err := trustedBackstore.Put(AccountKeyType, accKey) + if err != nil { + return nil, fmt.Errorf("cannot predefine trusted account key %q for %q: %v", accKey.PublicKeyID(), accKey.AccountID(), err) + } + + case *Account: + acct := accepted + err := trustedBackstore.Put(AccountType, acct) + if err != nil { + return nil, fmt.Errorf("cannot predefine trusted account %q: %v", acct.DisplayName(), err) + } + default: + return nil, fmt.Errorf("cannot predefine trusted assertions that are not account-key or account: %s", a.Type().Name) + } + } + + otherPredefinedBackstore := NewMemoryBackstore() + + for _, a := range cfg.OtherPredefined { + err := otherPredefinedBackstore.Put(a.Type(), a) + if err != nil { + return nil, fmt.Errorf("cannot predefine assertion %v: %v", a.Ref(), err) + } + } + + checkers := cfg.Checkers + if len(checkers) == 0 { + checkers = DefaultCheckers + } + dbCheckers := make([]Checker, len(checkers)) + copy(dbCheckers, checkers) + + return &Database{ + bs: bs, + keypairMgr: keypairMgr, + trusted: trustedBackstore, + predefined: otherPredefinedBackstore, + // order here is relevant, Find* precedence and + // findAccountKey depend on it, trusted should win over the + // general backstore! + backstores: []Backstore{trustedBackstore, otherPredefinedBackstore, bs}, + checkers: dbCheckers, + }, nil +} + +// ImportKey stores the given private/public key pair. +func (db *Database) ImportKey(privKey PrivateKey) error { + return db.keypairMgr.Put(privKey) +} + +var ( + // for sanity checking of base64 hash strings + base64HashLike = regexp.MustCompile("^[[:alnum:]_-]*$") +) + +func (db *Database) safeGetPrivateKey(keyID string) (PrivateKey, error) { + if keyID == "" { + return nil, fmt.Errorf("key id is empty") + } + if !base64HashLike.MatchString(keyID) { + return nil, fmt.Errorf("key id contains unexpected chars: %q", keyID) + } + return db.keypairMgr.Get(keyID) +} + +// PublicKey returns the public key part of the key pair that has the given key id. +func (db *Database) PublicKey(keyID string) (PublicKey, error) { + privKey, err := db.safeGetPrivateKey(keyID) + if err != nil { + return nil, err + } + return privKey.PublicKey(), nil +} + +// Sign assembles an assertion with the provided information and signs it +// with the private key from `headers["authority-id"]` that has the provided key id. +func (db *Database) Sign(assertType *AssertionType, headers map[string]interface{}, body []byte, keyID string) (Assertion, error) { + privKey, err := db.safeGetPrivateKey(keyID) + if err != nil { + return nil, err + } + return assembleAndSign(assertType, headers, body, privKey) +} + +// findAccountKey finds an AccountKey exactly with account id and key id. +func (db *Database) findAccountKey(authorityID, keyID string) (*AccountKey, error) { + key := []string{keyID} + // consider trusted account keys then disk stored account keys + for _, bs := range db.backstores { + a, err := bs.Get(AccountKeyType, key, AccountKeyType.MaxSupportedFormat()) + if err == nil { + hit := a.(*AccountKey) + if hit.AccountID() != authorityID { + return nil, fmt.Errorf("found public key %q from %q but expected it from: %s", keyID, hit.AccountID(), authorityID) + } + return hit, nil + } + if !IsNotFound(err) { + return nil, err + } + } + return nil, &NotFoundError{Type: AccountKeyType} +} + +// IsTrustedAccount returns whether the account is part of the trusted set. +func (db *Database) IsTrustedAccount(accountID string) bool { + if accountID == "" { + return false + } + _, err := db.trusted.Get(AccountType, []string{accountID}, AccountType.MaxSupportedFormat()) + return err == nil +} + +// Check tests whether the assertion is properly signed and consistent with all the stored knowledge. +func (db *Database) Check(assert Assertion) error { + if !assert.SupportedFormat() { + return &UnsupportedFormatError{Ref: assert.Ref(), Format: assert.Format()} + } + + typ := assert.Type() + now := time.Now() + + var accKey *AccountKey + var err error + if typ.flags&noAuthority == 0 { + // TODO: later may need to consider type of assert to find candidate keys + accKey, err = db.findAccountKey(assert.AuthorityID(), assert.SignKeyID()) + if IsNotFound(err) { + return fmt.Errorf("no matching public key %q for signature by %q", assert.SignKeyID(), assert.AuthorityID()) + } + if err != nil { + return fmt.Errorf("error finding matching public key for signature: %v", err) + } + } else { + if assert.AuthorityID() != "" { + return fmt.Errorf("internal error: %q assertion cannot have authority-id set", typ.Name) + } + } + + for _, checker := range db.checkers { + err := checker(assert, accKey, db, now) + if err != nil { + return err + } + } + + return nil +} + +// Add persists the assertion after ensuring it is properly signed and consistent with all the stored knowledge. +// It will return an error when trying to add an older revision of the assertion than the one currently stored. +func (db *Database) Add(assert Assertion) error { + ref := assert.Ref() + + if len(ref.PrimaryKey) == 0 { + return fmt.Errorf("internal error: assertion type %q has no primary key", ref.Type.Name) + } + + err := db.Check(assert) + if err != nil { + if ufe, ok := err.(*UnsupportedFormatError); ok { + _, err := ref.Resolve(db.Find) + if err != nil && !IsNotFound(err) { + return err + } + return &UnsupportedFormatError{Ref: ufe.Ref, Format: ufe.Format, Update: err == nil} + } + return err + } + + for i, keyVal := range ref.PrimaryKey { + if keyVal == "" { + return fmt.Errorf("missing or non-string primary key header: %v", ref.Type.PrimaryKey[i]) + } + } + + // assuming trusted account keys/assertions will be managed + // through the os snap this seems the safest policy until we + // know more/better + _, err = db.trusted.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat()) + if !IsNotFound(err) { + return fmt.Errorf("cannot add %q assertion with primary key clashing with a trusted assertion: %v", ref.Type.Name, ref.PrimaryKey) + } + + _, err = db.predefined.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat()) + if !IsNotFound(err) { + return fmt.Errorf("cannot add %q assertion with primary key clashing with a predefined assertion: %v", ref.Type.Name, ref.PrimaryKey) + } + + return db.bs.Put(ref.Type, assert) +} + +func searchMatch(assert Assertion, expectedHeaders map[string]string) bool { + // check non-primary-key headers as well + for expectedKey, expectedValue := range expectedHeaders { + if assert.Header(expectedKey) != expectedValue { + return false + } + } + return true +} + +func find(backstores []Backstore, assertionType *AssertionType, headers map[string]string, maxFormat int) (Assertion, error) { + err := checkAssertType(assertionType) + if err != nil { + return nil, err + } + maxSupp := assertionType.MaxSupportedFormat() + if maxFormat == -1 { + maxFormat = maxSupp + } else { + if maxFormat > maxSupp { + return nil, fmt.Errorf("cannot find %q assertions for format %d higher than supported format %d", assertionType.Name, maxFormat, maxSupp) + } + } + + keyValues, err := PrimaryKeyFromHeaders(assertionType, headers) + if err != nil { + return nil, err + } + + var assert Assertion + for _, bs := range backstores { + a, err := bs.Get(assertionType, keyValues, maxFormat) + if err == nil { + assert = a + break + } + if !IsNotFound(err) { + return nil, err + } + } + + if assert == nil || !searchMatch(assert, headers) { + return nil, &NotFoundError{Type: assertionType, Headers: headers} + } + + return assert, nil +} + +// Find an assertion based on arbitrary headers. +// Provided headers must contain the primary key for the assertion type. +// It returns a NotFoundError if the assertion cannot be found. +func (db *Database) Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) { + return find(db.backstores, assertionType, headers, -1) +} + +// FindMaxFormat finds an assertion like Find but such that its +// format is <= maxFormat by passing maxFormat along to the backend. +// It returns a NotFoundError if such an assertion cannot be found. +func (db *Database) FindMaxFormat(assertionType *AssertionType, headers map[string]string, maxFormat int) (Assertion, error) { + return find(db.backstores, assertionType, headers, maxFormat) +} + +// FindPredefined finds an assertion in the predefined sets (trusted +// or not) based on arbitrary headers. Provided headers must contain +// the primary key for the assertion type. It returns a NotFoundError +// if the assertion cannot be found. +func (db *Database) FindPredefined(assertionType *AssertionType, headers map[string]string) (Assertion, error) { + return find([]Backstore{db.trusted, db.predefined}, assertionType, headers, -1) +} + +// FindTrusted finds an assertion in the trusted set based on arbitrary headers. +// Provided headers must contain the primary key for the assertion type. +// It returns a NotFoundError if the assertion cannot be found. +func (db *Database) FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) { + return find([]Backstore{db.trusted}, assertionType, headers, -1) +} + +func (db *Database) findMany(backstores []Backstore, assertionType *AssertionType, headers map[string]string) ([]Assertion, error) { + err := checkAssertType(assertionType) + if err != nil { + return nil, err + } + res := []Assertion{} + + foundCb := func(assert Assertion) { + res = append(res, assert) + } + + // TODO: Find variant taking this + maxFormat := assertionType.MaxSupportedFormat() + for _, bs := range backstores { + err = bs.Search(assertionType, headers, foundCb, maxFormat) + if err != nil { + return nil, err + } + } + + if len(res) == 0 { + return nil, &NotFoundError{Type: assertionType, Headers: headers} + } + return res, nil +} + +// FindMany finds assertions based on arbitrary headers. +// It returns a NotFoundError if no assertion can be found. +func (db *Database) FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) { + return db.findMany(db.backstores, assertionType, headers) +} + +// FindManyPrefined finds assertions in the predefined sets (trusted +// or not) based on arbitrary headers. It returns a NotFoundError if +// no assertion can be found. +func (db *Database) FindManyPredefined(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) { + return db.findMany([]Backstore{db.trusted, db.predefined}, assertionType, headers) +} + +// assertion checkers + +// CheckSigningKeyIsNotExpired checks that the signing key is not expired. +func CheckSigningKeyIsNotExpired(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error { + if signingKey == nil { + // assert isn't signed with an account-key key, CheckSignature + // will fail anyway unless we teach it more stuff, + // Also this check isn't so relevant for self-signed asserts + // (e.g. account-key-request) + return nil + } + if !signingKey.isKeyValidAt(checkTime) { + return fmt.Errorf("assertion is signed with expired public key %q from %q", assert.SignKeyID(), assert.AuthorityID()) + } + return nil +} + +// CheckSignature checks that the signature is valid. +func CheckSignature(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error { + var pubKey PublicKey + if signingKey != nil { + pubKey = signingKey.publicKey() + } else { + custom, ok := assert.(customSigner) + if !ok { + return fmt.Errorf("cannot check no-authority assertion type %q", assert.Type().Name) + } + pubKey = custom.signKey() + } + content, encSig := assert.Signature() + signature, err := decodeSignature(encSig) + if err != nil { + return err + } + err = pubKey.verify(content, signature) + if err != nil { + return fmt.Errorf("failed signature verification: %v", err) + } + return nil +} + +type timestamped interface { + Timestamp() time.Time +} + +// CheckTimestampVsSigningKeyValidity verifies that the timestamp of +// the assertion is within the signing key validity. +func CheckTimestampVsSigningKeyValidity(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error { + if signingKey == nil { + // assert isn't signed with an account-key key, CheckSignature + // will fail anyway unless we teach it more stuff. + // Also this check isn't so relevant for self-signed asserts + // (e.g. account-key-request) + return nil + } + if tstamped, ok := assert.(timestamped); ok { + checkTime := tstamped.Timestamp() + if !signingKey.isKeyValidAt(checkTime) { + until := "" + if !signingKey.Until().IsZero() { + until = fmt.Sprintf(" until %q", signingKey.Until()) + } + return fmt.Errorf("%s assertion timestamp outside of signing key validity (key valid since %q%s)", assert.Type().Name, signingKey.Since(), until) + } + } + return nil +} + +// XXX: keeping these in this form until we know better + +// A consistencyChecker performs further checks based on the full +// assertion database knowledge and its own signing key. +type consistencyChecker interface { + checkConsistency(roDB RODatabase, signingKey *AccountKey) error +} + +// CheckCrossConsistency verifies that the assertion is consistent with the other statements in the database. +func CheckCrossConsistency(assert Assertion, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error { + // see if the assertion requires further checks + if checker, ok := assert.(consistencyChecker); ok { + return checker.checkConsistency(roDB, signingKey) + } + return nil +} + +// DefaultCheckers lists the default and recommended assertion +// checkers used by Database if none are specified in the +// DatabaseConfig.Checkers. +var DefaultCheckers = []Checker{ + CheckSigningKeyIsNotExpired, + CheckSignature, + CheckTimestampVsSigningKeyValidity, + CheckCrossConsistency, +} diff --git a/asserts/database_test.go b/asserts/database_test.go new file mode 100644 index 00000000..53708774 --- /dev/null +++ b/asserts/database_test.go @@ -0,0 +1,1190 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "bytes" + "crypto" + "encoding/base64" + "errors" + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" + "time" + + "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/sha3" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +func Test(t *testing.T) { TestingT(t) } + +var _ = Suite(&openSuite{}) +var _ = Suite(&revisionErrorSuite{}) + +type openSuite struct{} + +func (opens *openSuite) TestOpenDatabaseOK(c *C) { + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + c.Assert(db, NotNil) +} + +func (opens *openSuite) TestOpenDatabaseTrustedAccount(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "trusted", + "display-name": "Trusted", + "validation": "certified", + "timestamp": "2015-01-01T14:00:00Z", + } + acct, err := asserts.AssembleAndSignInTest(asserts.AccountType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: []asserts.Assertion{acct}, + } + + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + a, err := db.Find(asserts.AccountType, map[string]string{ + "account-id": "trusted", + }) + c.Assert(err, IsNil) + acct1 := a.(*asserts.Account) + c.Check(acct1.AccountID(), Equals, "trusted") + c.Check(acct1.DisplayName(), Equals, "Trusted") + + c.Check(db.IsTrustedAccount("trusted"), Equals, true) + + // empty account id (invalid) is not trusted + c.Check(db.IsTrustedAccount(""), Equals, false) +} + +func (opens *openSuite) TestOpenDatabaseTrustedWrongType(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "0", + } + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + cfg := &asserts.DatabaseConfig{ + Trusted: []asserts.Assertion{a}, + } + + _, err = asserts.OpenDatabase(cfg) + c.Assert(err, ErrorMatches, "cannot predefine trusted assertions that are not account-key or account: test-only") +} + +type databaseSuite struct { + topDir string + db *asserts.Database +} + +var _ = Suite(&databaseSuite{}) + +func (dbs *databaseSuite) SetUpTest(c *C) { + dbs.topDir = filepath.Join(c.MkDir(), "asserts-db") + fsKeypairMgr, err := asserts.OpenFSKeypairManager(dbs.topDir) + c.Assert(err, IsNil) + cfg := &asserts.DatabaseConfig{ + KeypairManager: fsKeypairMgr, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + dbs.db = db +} + +func (dbs *databaseSuite) TestImportKey(c *C) { + err := dbs.db.ImportKey(testPrivKey1) + c.Assert(err, IsNil) + + keyPath := filepath.Join(dbs.topDir, "private-keys-v1", testPrivKey1SHA3_384) + info, err := os.Stat(keyPath) + c.Assert(err, IsNil) + c.Check(info.Mode().Perm(), Equals, os.FileMode(0600)) // secret + // too white box? ok at least until we have more functionality + privKey, err := ioutil.ReadFile(keyPath) + c.Assert(err, IsNil) + + privKeyFromDisk, err := asserts.DecodePrivateKeyInTest(privKey) + c.Assert(err, IsNil) + + c.Check(privKeyFromDisk.PublicKey().ID(), Equals, testPrivKey1SHA3_384) +} + +func (dbs *databaseSuite) TestImportKeyAlreadyExists(c *C) { + err := dbs.db.ImportKey(testPrivKey1) + c.Assert(err, IsNil) + + err = dbs.db.ImportKey(testPrivKey1) + c.Check(err, ErrorMatches, "key pair with given key id already exists") +} + +func (dbs *databaseSuite) TestPublicKey(c *C) { + pk := testPrivKey1 + keyID := pk.PublicKey().ID() + err := dbs.db.ImportKey(pk) + c.Assert(err, IsNil) + + pubk, err := dbs.db.PublicKey(keyID) + c.Assert(err, IsNil) + c.Check(pubk.ID(), Equals, keyID) + + // usual pattern is to then encode it + encoded, err := asserts.EncodePublicKey(pubk) + c.Assert(err, IsNil) + data, err := base64.StdEncoding.DecodeString(string(encoded)) + c.Assert(err, IsNil) + c.Check(data[0], Equals, uint8(1)) // v1 + + // check details of packet + const newHeaderBits = 0x80 | 0x40 + c.Check(data[1]&newHeaderBits, Equals, uint8(newHeaderBits)) + c.Check(data[2] < 192, Equals, true) // small packet, 1 byte length + c.Check(data[3], Equals, uint8(4)) // openpgp v4 + pkt, err := packet.Read(bytes.NewBuffer(data[1:])) + c.Assert(err, IsNil) + pubKey, ok := pkt.(*packet.PublicKey) + c.Assert(ok, Equals, true) + c.Check(pubKey.PubKeyAlgo, Equals, packet.PubKeyAlgoRSA) + c.Check(pubKey.IsSubkey, Equals, false) + fixedTimestamp := time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC) + c.Check(pubKey.CreationTime.Equal(fixedTimestamp), Equals, true) + // hash of blob content == hash of key + h384 := sha3.Sum384(data) + encHash := base64.RawURLEncoding.EncodeToString(h384[:]) + c.Check(encHash, DeepEquals, testPrivKey1SHA3_384) +} + +func (dbs *databaseSuite) TestPublicKeyNotFound(c *C) { + pk := testPrivKey1 + keyID := pk.PublicKey().ID() + + _, err := dbs.db.PublicKey(keyID) + c.Check(err, ErrorMatches, "cannot find key pair") + + err = dbs.db.ImportKey(pk) + c.Assert(err, IsNil) + + _, err = dbs.db.PublicKey("ff" + keyID) + c.Check(err, ErrorMatches, "cannot find key pair") +} + +type checkSuite struct { + bs asserts.Backstore + a asserts.Assertion +} + +var _ = Suite(&checkSuite{}) + +func (chks *checkSuite) SetUpTest(c *C) { + var err error + + topDir := filepath.Join(c.MkDir(), "asserts-db") + chks.bs, err = asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "0", + } + chks.a, err = asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) +} + +func (chks *checkSuite) TestCheckNoPubKey(c *C) { + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + err = db.Check(chks.a) + c.Assert(err, ErrorMatches, `no matching public key "[[:alnum:]_-]+" for signature by "canonical"`) +} + +func (chks *checkSuite) TestCheckExpiredPubKey(c *C) { + trustedKey := testPrivKey0 + + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + Trusted: []asserts.Assertion{asserts.ExpiredAccountKeyForTest("canonical", trustedKey.PublicKey())}, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + err = db.Check(chks.a) + c.Assert(err, ErrorMatches, `assertion is signed with expired public key "[[:alnum:]_-]+" from "canonical"`) +} + +func (chks *checkSuite) TestCheckForgery(c *C) { + trustedKey := testPrivKey0 + + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + Trusted: []asserts.Assertion{asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey())}, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + encoded := asserts.Encode(chks.a) + content, encodedSig := chks.a.Signature() + // forgery + forgedSig := new(packet.Signature) + forgedSig.PubKeyAlgo = packet.PubKeyAlgoRSA + forgedSig.Hash = crypto.SHA512 + forgedSig.CreationTime = time.Now() + h := crypto.SHA512.New() + h.Write(content) + pk1 := packet.NewRSAPrivateKey(time.Unix(1, 0), testPrivKey1RSA) + err = forgedSig.Sign(h, pk1, &packet.Config{DefaultHash: crypto.SHA512}) + c.Assert(err, IsNil) + buf := new(bytes.Buffer) + forgedSig.Serialize(buf) + b := append([]byte{0x1}, buf.Bytes()...) + forgedSigEncoded := base64.StdEncoding.EncodeToString(b) + forgedEncoded := bytes.Replace(encoded, encodedSig, []byte(forgedSigEncoded), 1) + c.Assert(forgedEncoded, Not(DeepEquals), encoded) + + forgedAssert, err := asserts.Decode(forgedEncoded) + c.Assert(err, IsNil) + + err = db.Check(forgedAssert) + c.Assert(err, ErrorMatches, "failed signature verification: .*") +} + +func (chks *checkSuite) TestCheckUnsupportedFormat(c *C) { + trustedKey := testPrivKey0 + + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + Trusted: []asserts.Assertion{asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey())}, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + var a asserts.Assertion + (func() { + restore := asserts.MockMaxSupportedFormat(asserts.TestOnlyType, 77) + defer restore() + var err error + + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "0", + "format": "77", + } + a, err = asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, trustedKey) + c.Assert(err, IsNil) + })() + + err = db.Check(a) + c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{}) + c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported`) +} + +type signAddFindSuite struct { + signingDB *asserts.Database + signingKeyID string + db *asserts.Database +} + +var _ = Suite(&signAddFindSuite{}) + +func (safs *signAddFindSuite) SetUpTest(c *C) { + cfg0 := &asserts.DatabaseConfig{} + db0, err := asserts.OpenDatabase(cfg0) + c.Assert(err, IsNil) + safs.signingDB = db0 + + pk := testPrivKey0 + err = db0.ImportKey(pk) + c.Assert(err, IsNil) + safs.signingKeyID = pk.PublicKey().ID() + + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "type": "account", + "authority-id": "canonical", + "account-id": "predefined", + "validation": "certified", + "display-name": "Predef", + "timestamp": time.Now().Format(time.RFC3339), + } + predefAcct, err := safs.signingDB.Sign(asserts.AccountType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + trustedKey := testPrivKey0 + cfg := &asserts.DatabaseConfig{ + Backstore: bs, + Trusted: []asserts.Assertion{ + asserts.BootstrapAccountForTest("canonical"), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey.PublicKey()), + }, + OtherPredefined: []asserts.Assertion{ + predefAcct, + }, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + safs.db = db +} + +func (safs *signAddFindSuite) TestSign(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Check(a1) + c.Check(err, IsNil) +} + +func (safs *signAddFindSuite) TestSignEmptyKeyID(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, "") + c.Assert(err, ErrorMatches, "key id is empty") + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignMissingAuthorityId(c *C) { + headers := map[string]interface{}{ + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"authority-id" header is mandatory`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignMissingPrimaryKey(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"primary-key" header is mandatory`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignPrimaryKeyWithSlash(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "baz/9000", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"primary-key" primary key header cannot contain '/'`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignNoPrivateKey(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, "abcd") + c.Assert(err, ErrorMatches, "cannot find key pair") + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignUnknownType(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + } + a1, err := safs.signingDB.Sign(&asserts.AssertionType{Name: "xyz", PrimaryKey: nil}, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `internal error: unknown assertion type: "xyz"`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignNonPredefinedType(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + } + a1, err := safs.signingDB.Sign(&asserts.AssertionType{Name: "test-only", PrimaryKey: nil}, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `internal error: unpredefined assertion type for name "test-only" used.*`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignBadRevision(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "revision": "zzz", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"revision" header is not an integer: zzz`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignBadFormat(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "format": "zzz", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `"format" header is not an integer: zzz`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignHeadersCheck(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "extra": []interface{}{1, 2}, + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Check(err, ErrorMatches, `header "extra": header values must be strings or nested lists or maps with strings as the only scalars: 1`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignHeadersCheckMap(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "extra": map[string]interface{}{"a": "a", "b": 1}, + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Check(err, ErrorMatches, `header "extra": header values must be strings or nested lists or maps with strings as the only scalars: 1`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignAssemblerError(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "count": "zzz", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `cannot assemble assertion test-only: "count" header is not an integer: zzz`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignUnsupportedFormat(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "format": "77", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `cannot sign "test-only" assertion with format 77 higher than max supported format 1`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestSignInadequateFormat(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "format-1-feature": "true", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, ErrorMatches, `cannot sign "test-only" assertion with format set to 0 lower than min format 1 covering included features`) + c.Check(a1, IsNil) +} + +func (safs *signAddFindSuite) TestAddRefusesSelfSignedKey(c *C) { + aKey := testPrivKey2 + + aKeyEncoded, err := asserts.EncodePublicKey(aKey.PublicKey()) + c.Assert(err, IsNil) + + now := time.Now().UTC() + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "canonical", + "public-key-sha3-384": aKey.PublicKey().ID(), + "name": "default", + "since": now.Format(time.RFC3339), + } + acctKey, err := asserts.AssembleAndSignInTest(asserts.AccountKeyType, headers, aKeyEncoded, aKey) + c.Assert(err, IsNil) + + // this must fail + err = safs.db.Add(acctKey) + c.Check(err, ErrorMatches, `no matching public key.*`) +} + +func (safs *signAddFindSuite) TestAddSuperseding(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a1) + c.Assert(err, IsNil) + + retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "a", + }) + c.Assert(err, IsNil) + c.Check(retrieved1, NotNil) + c.Check(retrieved1.Revision(), Equals, 0) + + headers["revision"] = "1" + a2, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a2) + c.Assert(err, IsNil) + + retrieved2, err := safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "a", + }) + c.Assert(err, IsNil) + c.Check(retrieved2, NotNil) + c.Check(retrieved2.Revision(), Equals, 1) + + err = safs.db.Add(a1) + c.Check(err, ErrorMatches, "revision 0 is older than current revision 1") + c.Check(asserts.IsUnaccceptedUpdate(err), Equals, true) +} + +func (safs *signAddFindSuite) TestAddNoAuthorityNoPrimaryKey(c *C) { + headers := map[string]interface{}{ + "hdr": "FOO", + } + a, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = safs.db.Add(a) + c.Assert(err, ErrorMatches, `internal error: assertion type "test-only-no-authority" has no primary key`) +} + +func (safs *signAddFindSuite) TestAddNoAuthorityButPrimaryKey(c *C) { + headers := map[string]interface{}{ + "pk": "primary", + } + a, err := asserts.SignWithoutAuthority(asserts.TestOnlyNoAuthorityPKType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = safs.db.Add(a) + c.Assert(err, ErrorMatches, `cannot check no-authority assertion type "test-only-no-authority-pk"`) +} + +func (safs *signAddFindSuite) TestAddUnsupportedFormat(c *C) { + const unsupported = "type: test-only\n" + + "format: 77\n" + + "authority-id: canonical\n" + + "primary-key: a\n" + + "payload: unsupported\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + aUnsupp, err := asserts.Decode([]byte(unsupported)) + c.Assert(err, IsNil) + c.Assert(aUnsupp.SupportedFormat(), Equals, false) + + err = safs.db.Add(aUnsupp) + c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{}) + c.Check(err.(*asserts.UnsupportedFormatError).Update, Equals, false) + c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported`) + c.Check(asserts.IsUnaccceptedUpdate(err), Equals, false) + + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "format": "1", + "payload": "supported", + } + aSupp, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = safs.db.Add(aSupp) + c.Assert(err, IsNil) + + err = safs.db.Add(aUnsupp) + c.Assert(err, FitsTypeOf, &asserts.UnsupportedFormatError{}) + c.Check(err.(*asserts.UnsupportedFormatError).Update, Equals, true) + c.Check(err, ErrorMatches, `proposed "test-only" assertion has format 77 but 1 is latest supported \(current not updated\)`) + c.Check(asserts.IsUnaccceptedUpdate(err), Equals, true) +} + +func (safs *signAddFindSuite) TestNotFoundError(c *C) { + err1 := &asserts.NotFoundError{ + Type: asserts.SnapDeclarationType, + Headers: map[string]string{ + "series": "16", + "snap-id": "snap-id", + }, + } + c.Check(asserts.IsNotFound(err1), Equals, true) + c.Check(err1.Error(), Equals, "snap-declaration (snap-id; series:16) not found") + + err2 := &asserts.NotFoundError{ + Type: asserts.SnapRevisionType, + } + c.Check(asserts.IsNotFound(err1), Equals, true) + c.Check(err2.Error(), Equals, "snap-revision assertion not found") +} + +func (safs *signAddFindSuite) TestFindNotFound(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a1) + c.Assert(err, IsNil) + + hdrs := map[string]string{ + "primary-key": "b", + } + retrieved1, err := safs.db.Find(asserts.TestOnlyType, hdrs) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: hdrs, + }) + c.Check(retrieved1, IsNil) + + // checking also extra headers + hdrs = map[string]string{ + "primary-key": "a", + "authority-id": "other-auth-id", + } + retrieved1, err = safs.db.Find(asserts.TestOnlyType, hdrs) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: hdrs, + }) + c.Check(retrieved1, IsNil) +} + +func (safs *signAddFindSuite) TestFindPrimaryLeftOut(c *C) { + retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{}) + c.Assert(err, ErrorMatches, "must provide primary key: primary-key") + c.Check(retrieved1, IsNil) +} + +func (safs *signAddFindSuite) TestFindMany(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "a", + "other": "other-x", + } + aa, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + err = safs.db.Add(aa) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "b", + "other": "other-y", + } + ab, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + err = safs.db.Add(ab) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "c", + "other": "other-x", + } + ac, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + err = safs.db.Add(ac) + c.Assert(err, IsNil) + + res, err := safs.db.FindMany(asserts.TestOnlyType, map[string]string{ + "other": "other-x", + }) + c.Assert(err, IsNil) + c.Assert(res, HasLen, 2) + primKeys := []string{res[0].HeaderString("primary-key"), res[1].HeaderString("primary-key")} + sort.Strings(primKeys) + c.Check(primKeys, DeepEquals, []string{"a", "c"}) + + res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{ + "other": "other-y", + }) + c.Assert(err, IsNil) + c.Assert(res, HasLen, 1) + c.Check(res[0].Header("primary-key"), Equals, "b") + + res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{}) + c.Assert(err, IsNil) + c.Assert(res, HasLen, 3) + + res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{ + "primary-key": "b", + "other": "other-y", + }) + c.Assert(err, IsNil) + c.Assert(res, HasLen, 1) + + hdrs := map[string]string{ + "primary-key": "b", + "other": "other-x", + } + res, err = safs.db.FindMany(asserts.TestOnlyType, hdrs) + c.Assert(res, HasLen, 0) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: hdrs, + }) +} + +func (safs *signAddFindSuite) TestFindFindsPredefined(c *C) { + pk1 := testPrivKey1 + + acct1 := assertstest.NewAccount(safs.signingDB, "acc-id1", map[string]interface{}{ + "authority-id": "canonical", + }, safs.signingKeyID) + + acct1Key := assertstest.NewAccountKey(safs.signingDB, acct1, map[string]interface{}{ + "authority-id": "canonical", + }, pk1.PublicKey(), safs.signingKeyID) + + err := safs.db.Add(acct1) + c.Assert(err, IsNil) + err = safs.db.Add(acct1Key) + c.Assert(err, IsNil) + + // find the trusted key as well + tKey, err := safs.db.Find(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + "public-key-sha3-384": safs.signingKeyID, + }) + c.Assert(err, IsNil) + c.Assert(tKey.(*asserts.AccountKey).AccountID(), Equals, "canonical") + c.Assert(tKey.(*asserts.AccountKey).PublicKeyID(), Equals, safs.signingKeyID) + + // find predefined account as well + predefAcct, err := safs.db.Find(asserts.AccountType, map[string]string{ + "account-id": "predefined", + }) + c.Assert(err, IsNil) + c.Assert(predefAcct.(*asserts.Account).AccountID(), Equals, "predefined") + c.Assert(predefAcct.(*asserts.Account).DisplayName(), Equals, "Predef") + + // find trusted and indirectly trusted + accKeys, err := safs.db.FindMany(asserts.AccountKeyType, nil) + c.Assert(err, IsNil) + c.Check(accKeys, HasLen, 2) + + accts, err := safs.db.FindMany(asserts.AccountType, nil) + c.Assert(err, IsNil) + c.Check(accts, HasLen, 3) +} + +func (safs *signAddFindSuite) TestFindTrusted(c *C) { + pk1 := testPrivKey1 + + acct1 := assertstest.NewAccount(safs.signingDB, "acc-id1", map[string]interface{}{ + "authority-id": "canonical", + }, safs.signingKeyID) + + acct1Key := assertstest.NewAccountKey(safs.signingDB, acct1, map[string]interface{}{ + "authority-id": "canonical", + }, pk1.PublicKey(), safs.signingKeyID) + + err := safs.db.Add(acct1) + c.Assert(err, IsNil) + err = safs.db.Add(acct1Key) + c.Assert(err, IsNil) + + // find the trusted account + tAcct, err := safs.db.FindTrusted(asserts.AccountType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + c.Assert(tAcct.(*asserts.Account).AccountID(), Equals, "canonical") + + // find the trusted key + tKey, err := safs.db.FindTrusted(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + "public-key-sha3-384": safs.signingKeyID, + }) + c.Assert(err, IsNil) + c.Assert(tKey.(*asserts.AccountKey).AccountID(), Equals, "canonical") + c.Assert(tKey.(*asserts.AccountKey).PublicKeyID(), Equals, safs.signingKeyID) + + // doesn't find not trusted assertions + hdrs := map[string]string{ + "account-id": acct1.AccountID(), + } + _, err = safs.db.FindTrusted(asserts.AccountType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountType, + Headers: hdrs, + }) + + hdrs = map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + } + _, err = safs.db.FindTrusted(asserts.AccountKeyType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountKeyType, + Headers: hdrs, + }) + + _, err = safs.db.FindTrusted(asserts.AccountType, map[string]string{ + "account-id": "predefined", + }) + c.Check(asserts.IsNotFound(err), Equals, true) +} + +func (safs *signAddFindSuite) TestFindPredefined(c *C) { + pk1 := testPrivKey1 + + acct1 := assertstest.NewAccount(safs.signingDB, "acc-id1", map[string]interface{}{ + "authority-id": "canonical", + }, safs.signingKeyID) + + acct1Key := assertstest.NewAccountKey(safs.signingDB, acct1, map[string]interface{}{ + "authority-id": "canonical", + }, pk1.PublicKey(), safs.signingKeyID) + + err := safs.db.Add(acct1) + c.Assert(err, IsNil) + err = safs.db.Add(acct1Key) + c.Assert(err, IsNil) + + // find the trusted account + tAcct, err := safs.db.FindPredefined(asserts.AccountType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + c.Assert(tAcct.(*asserts.Account).AccountID(), Equals, "canonical") + + // find the trusted key + tKey, err := safs.db.FindPredefined(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + "public-key-sha3-384": safs.signingKeyID, + }) + c.Assert(err, IsNil) + c.Assert(tKey.(*asserts.AccountKey).AccountID(), Equals, "canonical") + c.Assert(tKey.(*asserts.AccountKey).PublicKeyID(), Equals, safs.signingKeyID) + + // find predefined account as well + predefAcct, err := safs.db.FindPredefined(asserts.AccountType, map[string]string{ + "account-id": "predefined", + }) + c.Assert(err, IsNil) + c.Assert(predefAcct.(*asserts.Account).AccountID(), Equals, "predefined") + c.Assert(predefAcct.(*asserts.Account).DisplayName(), Equals, "Predef") + + // doesn't find not trusted or predefined assertions + hdrs := map[string]string{ + "account-id": acct1.AccountID(), + } + _, err = safs.db.FindPredefined(asserts.AccountType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountType, + Headers: hdrs, + }) + + hdrs = map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + } + _, err = safs.db.FindPredefined(asserts.AccountKeyType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountKeyType, + Headers: hdrs, + }) +} + +func (safs *signAddFindSuite) TestFindManyPredefined(c *C) { + headers := map[string]interface{}{ + "type": "account", + "authority-id": "canonical", + "account-id": "predefined", + "validation": "certified", + "display-name": "Predef", + "timestamp": time.Now().Format(time.RFC3339), + } + predefAcct, err := safs.signingDB.Sign(asserts.AccountType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + trustedKey0 := testPrivKey0 + trustedKey1 := testPrivKey1 + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: []asserts.Assertion{ + asserts.BootstrapAccountForTest("canonical"), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey0.PublicKey()), + asserts.BootstrapAccountKeyForTest("canonical", trustedKey1.PublicKey()), + }, + OtherPredefined: []asserts.Assertion{ + predefAcct, + }, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + pk1 := testPrivKey2 + + acct1 := assertstest.NewAccount(safs.signingDB, "acc-id1", map[string]interface{}{ + "authority-id": "canonical", + }, safs.signingKeyID) + + acct1Key := assertstest.NewAccountKey(safs.signingDB, acct1, map[string]interface{}{ + "authority-id": "canonical", + }, pk1.PublicKey(), safs.signingKeyID) + + err = db.Add(acct1) + c.Assert(err, IsNil) + err = db.Add(acct1Key) + c.Assert(err, IsNil) + + // find the trusted account + tAccts, err := db.FindManyPredefined(asserts.AccountType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + c.Assert(tAccts, HasLen, 1) + c.Assert(tAccts[0].(*asserts.Account).AccountID(), Equals, "canonical") + + // find the predefined account + pAccts, err := db.FindManyPredefined(asserts.AccountType, map[string]string{ + "account-id": "predefined", + }) + c.Assert(err, IsNil) + c.Assert(pAccts, HasLen, 1) + c.Assert(pAccts[0].(*asserts.Account).AccountID(), Equals, "predefined") + + // find the multiple trusted keys + tKeys, err := db.FindManyPredefined(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + c.Assert(tKeys, HasLen, 2) + got := make(map[string]string) + for _, a := range tKeys { + acctKey := a.(*asserts.AccountKey) + got[acctKey.PublicKeyID()] = acctKey.AccountID() + } + c.Check(got, DeepEquals, map[string]string{ + trustedKey0.PublicKey().ID(): "canonical", + trustedKey1.PublicKey().ID(): "canonical", + }) + + // doesn't find not predefined assertions + hdrs := map[string]string{ + "account-id": acct1.AccountID(), + } + _, err = db.FindManyPredefined(asserts.AccountType, hdrs) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.AccountType, + Headers: hdrs, + }) + + _, err = db.FindManyPredefined(asserts.AccountKeyType, map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + }) + c.Check(asserts.IsNotFound(err), Equals, true) +} + +func (safs *signAddFindSuite) TestDontLetAddConfusinglyAssertionClashingWithTrustedOnes(c *C) { + // trusted + pubKey0, err := safs.signingDB.PublicKey(safs.signingKeyID) + c.Assert(err, IsNil) + pubKey0Encoded, err := asserts.EncodePublicKey(pubKey0) + c.Assert(err, IsNil) + + now := time.Now().UTC() + headers := map[string]interface{}{ + "authority-id": "canonical", + "account-id": "canonical", + "public-key-sha3-384": safs.signingKeyID, + "name": "default", + "since": now.Format(time.RFC3339), + "until": now.AddDate(1, 0, 0).Format(time.RFC3339), + } + tKey, err := safs.signingDB.Sign(asserts.AccountKeyType, headers, []byte(pubKey0Encoded), safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(tKey) + c.Check(err, ErrorMatches, `cannot add "account-key" assertion with primary key clashing with a trusted assertion: .*`) +} + +func (safs *signAddFindSuite) TestDontLetAddConfusinglyAssertionClashingWithPredefinedOnes(c *C) { + headers := map[string]interface{}{ + "type": "account", + "authority-id": "canonical", + "account-id": "predefined", + "validation": "certified", + "display-name": "Predef", + "timestamp": time.Now().Format(time.RFC3339), + } + predefAcct, err := safs.signingDB.Sign(asserts.AccountType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(predefAcct) + c.Check(err, ErrorMatches, `cannot add "account" assertion with primary key clashing with a predefined assertion: .*`) +} + +func (safs *signAddFindSuite) TestFindAndRefResolve(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "pk1": "ka", + "pk2": "kb", + } + a1, err := safs.signingDB.Sign(asserts.TestOnly2Type, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a1) + c.Assert(err, IsNil) + + ref := &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"ka", "kb"}, + } + + resolved, err := ref.Resolve(safs.db.Find) + c.Assert(err, IsNil) + c.Check(resolved.Headers(), DeepEquals, map[string]interface{}{ + "type": "test-only-2", + "authority-id": "canonical", + "pk1": "ka", + "pk2": "kb", + "sign-key-sha3-384": resolved.SignKeyID(), + }) + + ref = &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"kb", "ka"}, + } + _, err = ref.Resolve(safs.db.Find) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: ref.Type, + Headers: map[string]string{ + "pk1": "kb", + "pk2": "ka", + }, + }) +} + +func (safs *signAddFindSuite) TestFindMaxFormat(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "foo", + } + af0, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(af0) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "foo", + "format": "1", + "revision": "1", + } + af1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(af1) + c.Assert(err, IsNil) + + a, err := safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{ + "primary-key": "foo", + }, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + a, err = safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{ + "primary-key": "foo", + }, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + + a, err = safs.db.FindMaxFormat(asserts.TestOnlyType, map[string]string{ + "primary-key": "foo", + }, 3) + c.Check(err, ErrorMatches, `cannot find "test-only" assertions for format 3 higher than supported format 1`) +} + +type revisionErrorSuite struct{} + +func (res *revisionErrorSuite) TestErrorText(c *C) { + tests := []struct { + err error + expected string + }{ + // Invalid revisions. + {&asserts.RevisionError{Used: -1}, "assertion revision is unknown"}, + {&asserts.RevisionError{Used: -100}, "assertion revision is unknown"}, + {&asserts.RevisionError{Current: -1}, "assertion revision is unknown"}, + {&asserts.RevisionError{Current: -100}, "assertion revision is unknown"}, + {&asserts.RevisionError{Used: -1, Current: -1}, "assertion revision is unknown"}, + // Used == Current. + {&asserts.RevisionError{}, "revision 0 is already the current revision"}, + {&asserts.RevisionError{Used: 100, Current: 100}, "revision 100 is already the current revision"}, + // Used < Current. + {&asserts.RevisionError{Used: 1, Current: 2}, "revision 1 is older than current revision 2"}, + {&asserts.RevisionError{Used: 2, Current: 100}, "revision 2 is older than current revision 100"}, + // Used > Current. + {&asserts.RevisionError{Current: 1, Used: 2}, "revision 2 is more recent than current revision 1"}, + {&asserts.RevisionError{Current: 2, Used: 100}, "revision 100 is more recent than current revision 2"}, + } + + for _, test := range tests { + c.Check(test.err, ErrorMatches, test.expected) + } +} + +type isUnacceptedUpdateSuite struct{} + +func (s *isUnacceptedUpdateSuite) TestIsUnacceptedUpdate(c *C) { + tests := []struct { + err error + keptCurrent bool + }{ + {&asserts.UnsupportedFormatError{}, false}, + {&asserts.UnsupportedFormatError{Update: true}, true}, + {&asserts.RevisionError{Used: 1, Current: 1}, true}, + {&asserts.RevisionError{Used: 1, Current: 5}, true}, + {&asserts.RevisionError{Used: 3, Current: 1}, false}, + {errors.New("other error"), false}, + {&asserts.NotFoundError{Type: asserts.TestOnlyType}, false}, + } + + for _, t := range tests { + c.Check(asserts.IsUnaccceptedUpdate(t.err), Equals, t.keptCurrent, Commentf("%v", t.err)) + } +} diff --git a/asserts/device_asserts.go b/asserts/device_asserts.go new file mode 100644 index 00000000..6f2a8f1d --- /dev/null +++ b/asserts/device_asserts.go @@ -0,0 +1,444 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "regexp" + "strings" + "time" +) + +// Model holds a model assertion, which is a statement by a brand +// about the properties of a device model. +type Model struct { + assertionBase + classic bool + requiredSnaps []string + sysUserAuthority []string + timestamp time.Time +} + +// BrandID returns the brand identifier. Same as the authority id. +func (mod *Model) BrandID() string { + return mod.HeaderString("brand-id") +} + +// Model returns the model name identifier. +func (mod *Model) Model() string { + return mod.HeaderString("model") +} + +// DisplayName returns the human-friendly name of the model or +// falls back to Model if this was not set. +func (mod *Model) DisplayName() string { + display := mod.HeaderString("display-name") + if display == "" { + return mod.Model() + } + return display +} + +// Series returns the series of the core software the model uses. +func (mod *Model) Series() string { + return mod.HeaderString("series") +} + +// Classic returns whether the model is a classic system. +func (mod *Model) Classic() bool { + return mod.classic +} + +// Architecture returns the archicteture the model is based on. +func (mod *Model) Architecture() string { + return mod.HeaderString("architecture") +} + +// Gadget returns the gadget snap the model uses. +func (mod *Model) Gadget() string { + return mod.HeaderString("gadget") +} + +// Kernel returns the kernel snap the model uses. +func (mod *Model) Kernel() string { + return mod.HeaderString("kernel") +} + +// Store returns the snap store the model uses. +func (mod *Model) Store() string { + return mod.HeaderString("store") +} + +// RequiredSnaps returns the snaps that must be installed at all times and cannot be removed for this model. +func (mod *Model) RequiredSnaps() []string { + return mod.requiredSnaps +} + +// SystemUserAuthority returns the authority ids that are accepted as signers of system-user assertions for this model. Empty list means any. +func (mod *Model) SystemUserAuthority() []string { + return mod.sysUserAuthority +} + +// Timestamp returns the time when the model assertion was issued. +func (mod *Model) Timestamp() time.Time { + return mod.timestamp +} + +// Implement further consistency checks. +func (mod *Model) checkConsistency(db RODatabase, acck *AccountKey) error { + // TODO: double check trust level of authority depending on class and possibly allowed-modes + return nil +} + +// sanity +var _ consistencyChecker = (*Model)(nil) + +// limit model to only lowercase for now +var validModel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") + +func checkModel(headers map[string]interface{}) (string, error) { + s, err := checkStringMatches(headers, "model", validModel) + if err != nil { + return "", err + } + // TODO: support the concept of case insensitive/preserving string headers + if strings.ToLower(s) != s { + return "", fmt.Errorf(`"model" header cannot contain uppercase letters`) + } + return s, nil +} + +func checkAuthorityMatchesBrand(a Assertion) error { + typeName := a.Type().Name + authorityID := a.AuthorityID() + brand := a.HeaderString("brand-id") + if brand != authorityID { + return fmt.Errorf("authority-id and brand-id must match, %s assertions are expected to be signed by the brand: %q != %q", typeName, authorityID, brand) + } + return nil +} + +func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID string) ([]string, error) { + const name = "system-user-authority" + v, ok := headers[name] + if !ok { + return []string{brandID}, nil + } + switch x := v.(type) { + case string: + if x == "*" { + return nil, nil + } + case []interface{}: + lst, err := checkStringListMatches(headers, name, validAccountID) + if err == nil { + return lst, nil + } + } + return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name) +} + +var ( + modelMandatory = []string{"architecture", "gadget", "kernel"} + classicModelOptional = []string{"architecture", "gadget"} +) + +func assembleModel(assert assertionBase) (Assertion, error) { + err := checkAuthorityMatchesBrand(&assert) + if err != nil { + return nil, err + } + + _, err = checkModel(assert.headers) + if err != nil { + return nil, err + } + + classic, err := checkOptionalBool(assert.headers, "classic") + if err != nil { + return nil, err + } + + if classic { + if _, ok := assert.headers["kernel"]; ok { + return nil, fmt.Errorf("cannot specify a kernel with a classic model") + } + } + + checker := checkNotEmptyString + toCheck := modelMandatory + if classic { + checker = checkOptionalString + toCheck = classicModelOptional + } + + for _, h := range toCheck { + if _, err := checker(assert.headers, h); err != nil { + return nil, err + } + } + + // store is optional but must be a string, defaults to the ubuntu store + _, err = checkOptionalString(assert.headers, "store") + if err != nil { + return nil, err + } + + // display-name is optional but must be a string + _, err = checkOptionalString(assert.headers, "display-name") + if err != nil { + return nil, err + } + + reqSnaps, err := checkStringList(assert.headers, "required-snaps") + if err != nil { + return nil, err + } + + sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, assert.HeaderString("brand-id")) + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + // NB: + // * core is not supported at this time, it defaults to ubuntu-core + // in prepare-image until rename and/or introduction of the header. + // * some form of allowed-modes, class are postponed, + // + // prepare-image takes care of not allowing them for now + + // ignore extra headers and non-empty body for future compatibility + return &Model{ + assertionBase: assert, + classic: classic, + requiredSnaps: reqSnaps, + sysUserAuthority: sysUserAuthority, + timestamp: timestamp, + }, nil +} + +// Serial holds a serial assertion, which is a statement binding a +// device identity with the device public key. +type Serial struct { + assertionBase + timestamp time.Time + pubKey PublicKey +} + +// BrandID returns the brand identifier of the device. +func (ser *Serial) BrandID() string { + return ser.HeaderString("brand-id") +} + +// Model returns the model name identifier of the device. +func (ser *Serial) Model() string { + return ser.HeaderString("model") +} + +// Serial returns the serial identifier of the device, together with +// brand id and model they form the unique identifier of the device. +func (ser *Serial) Serial() string { + return ser.HeaderString("serial") +} + +// DeviceKey returns the public key of the device. +func (ser *Serial) DeviceKey() PublicKey { + return ser.pubKey +} + +// Timestamp returns the time when the serial assertion was issued. +func (ser *Serial) Timestamp() time.Time { + return ser.timestamp +} + +// TODO: implement further consistency checks for Serial but first review approach + +func assembleSerial(assert assertionBase) (Assertion, error) { + err := checkAuthorityMatchesBrand(&assert) + if err != nil { + return nil, err + } + + _, err = checkModel(assert.headers) + if err != nil { + return nil, err + } + + encodedKey, err := checkNotEmptyString(assert.headers, "device-key") + if err != nil { + return nil, err + } + pubKey, err := DecodePublicKey([]byte(encodedKey)) + if err != nil { + return nil, err + } + keyID, err := checkNotEmptyString(assert.headers, "device-key-sha3-384") + if err != nil { + return nil, err + } + if keyID != pubKey.ID() { + return nil, fmt.Errorf("device key does not match provided key id") + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + // ignore extra headers and non-empty body for future compatibility + return &Serial{ + assertionBase: assert, + timestamp: timestamp, + pubKey: pubKey, + }, nil +} + +// SerialRequest holds a serial-request assertion, which is a self-signed request to obtain a full device identity bound to the device public key. +type SerialRequest struct { + assertionBase + pubKey PublicKey +} + +// BrandID returns the brand identifier of the device making the request. +func (sreq *SerialRequest) BrandID() string { + return sreq.HeaderString("brand-id") +} + +// Model returns the model name identifier of the device making the request. +func (sreq *SerialRequest) Model() string { + return sreq.HeaderString("model") +} + +// Serial returns the optional proposed serial identifier for the device, the service taking the request might use it or ignore it. +func (sreq *SerialRequest) Serial() string { + return sreq.HeaderString("serial") +} + +// RequestID returns the id for the request, obtained from and to be presented to the serial signing service. +func (sreq *SerialRequest) RequestID() string { + return sreq.HeaderString("request-id") +} + +// DeviceKey returns the public key of the device making the request. +func (sreq *SerialRequest) DeviceKey() PublicKey { + return sreq.pubKey +} + +func assembleSerialRequest(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "brand-id") + if err != nil { + return nil, err + } + + _, err = checkModel(assert.headers) + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "request-id") + if err != nil { + return nil, err + } + + _, err = checkOptionalString(assert.headers, "serial") + if err != nil { + return nil, err + } + + encodedKey, err := checkNotEmptyString(assert.headers, "device-key") + if err != nil { + return nil, err + } + pubKey, err := DecodePublicKey([]byte(encodedKey)) + if err != nil { + return nil, err + } + + if pubKey.ID() != assert.SignKeyID() { + return nil, fmt.Errorf("device key does not match included signing key id") + } + + // ignore extra headers and non-empty body for future compatibility + return &SerialRequest{ + assertionBase: assert, + pubKey: pubKey, + }, nil +} + +// DeviceSessionRequest holds a device-session-request assertion, which is a request wrapping a store-provided nonce to start a session by a device signed with its key. +type DeviceSessionRequest struct { + assertionBase + timestamp time.Time +} + +// BrandID returns the brand identifier of the device making the request. +func (req *DeviceSessionRequest) BrandID() string { + return req.HeaderString("brand-id") +} + +// Model returns the model name identifier of the device making the request. +func (req *DeviceSessionRequest) Model() string { + return req.HeaderString("model") +} + +// Serial returns the serial identifier of the device making the request, +// together with brand id and model it forms the unique identifier of +// the device. +func (req *DeviceSessionRequest) Serial() string { + return req.HeaderString("serial") +} + +// Nonce returns the nonce obtained from store and to be presented when requesting a device session. +func (req *DeviceSessionRequest) Nonce() string { + return req.HeaderString("nonce") +} + +// Timestamp returns the time when the device-session-request was created. +func (req *DeviceSessionRequest) Timestamp() time.Time { + return req.timestamp +} + +func assembleDeviceSessionRequest(assert assertionBase) (Assertion, error) { + _, err := checkModel(assert.headers) + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "nonce") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + // ignore extra headers and non-empty body for future compatibility + return &DeviceSessionRequest{ + assertionBase: assert, + timestamp: timestamp, + }, nil +} diff --git a/asserts/device_asserts_test.go b/asserts/device_asserts_test.go new file mode 100644 index 00000000..7aeb8d86 --- /dev/null +++ b/asserts/device_asserts_test.go @@ -0,0 +1,576 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "strings" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +type modelSuite struct { + ts time.Time + tsLine string +} + +var ( + _ = Suite(&modelSuite{}) + _ = Suite(&serialSuite{}) +) + +func (mods *modelSuite) SetUpSuite(c *C) { + mods.ts = time.Now().Truncate(time.Second).UTC() + mods.tsLine = "timestamp: " + mods.ts.Format(time.RFC3339) + "\n" +} + +const ( + reqSnaps = "required-snaps:\n - foo\n - bar\n" + sysUserAuths = "system-user-authority: *\n" +) + +const ( + modelExample = "type: model\n" + + "authority-id: brand-id1\n" + + "series: 16\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "display-name: Baz 3000\n" + + "architecture: amd64\n" + + "gadget: brand-gadget\n" + + "kernel: baz-linux\n" + + "store: brand-store\n" + + sysUserAuths + + reqSnaps + + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + classicModelExample = "type: model\n" + + "authority-id: brand-id1\n" + + "series: 16\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "display-name: Baz 3000\n" + + "classic: true\n" + + "architecture: amd64\n" + + "gadget: brand-gadget\n" + + "store: brand-store\n" + + reqSnaps + + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +) + +func (mods *modelSuite) TestDecodeOK(c *C) { + encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.AuthorityID(), Equals, "brand-id1") + c.Check(model.Timestamp(), Equals, mods.ts) + c.Check(model.Series(), Equals, "16") + c.Check(model.BrandID(), Equals, "brand-id1") + c.Check(model.Model(), Equals, "baz-3000") + c.Check(model.DisplayName(), Equals, "Baz 3000") + c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.Kernel(), Equals, "baz-linux") + c.Check(model.Store(), Equals, "brand-store") + c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) + c.Check(model.SystemUserAuthority(), HasLen, 0) +} + +func (mods *modelSuite) TestDecodeStoreIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "store: brand-store\n", "store: \n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.Store(), Equals, "") + + encoded = strings.Replace(withTimestamp, "store: brand-store\n", "", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + c.Check(model.Store(), Equals, "") +} + +func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, "display-name: Baz 3000\n", "display-name: \n", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + // optional but we fallback to Model + c.Check(model.DisplayName(), Equals, "baz-3000") + + encoded = strings.Replace(withTimestamp, "display-name: Baz 3000\n", "", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + // optional but we fallback to Model + c.Check(model.DisplayName(), Equals, "baz-3000") +} + +func (mods *modelSuite) TestDecodeRequiredSnapsAreOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, reqSnaps, "", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + c.Check(model.RequiredSnaps(), HasLen, 0) +} + +func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, sysUserAuths, "", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model := a.(*asserts.Model) + // the default is just to accept the brand itself + c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1"}) + + encoded = strings.Replace(withTimestamp, sysUserAuths, "system-user-authority:\n - foo\n - bar\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + c.Check(model.SystemUserAuthority(), DeepEquals, []string{"foo", "bar"}) +} + +const ( + modelErrPrefix = "assertion model: " +) + +func (mods *modelSuite) TestDecodeInvalid(c *C) { + encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`}, + {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"brand-id: brand-id1\n", "brand-id: random\n", `authority-id and brand-id must match, model assertions are expected to be signed by the brand: "brand-id1" != "random"`}, + {"model: baz-3000\n", "", `"model" header is mandatory`}, + {"model: baz-3000\n", "model: \n", `"model" header should not be empty`}, + {"model: baz-3000\n", "model: baz/3000\n", `"model" primary key header cannot contain '/'`}, + // lift this restriction at a later point + {"model: baz-3000\n", "model: BAZ-3000\n", `"model" header cannot contain uppercase letters`}, + {"display-name: Baz 3000\n", "display-name:\n - xyz\n", `"display-name" header must be a string`}, + {"architecture: amd64\n", "", `"architecture" header is mandatory`}, + {"architecture: amd64\n", "architecture: \n", `"architecture" header should not be empty`}, + {"gadget: brand-gadget\n", "", `"gadget" header is mandatory`}, + {"gadget: brand-gadget\n", "gadget: \n", `"gadget" header should not be empty`}, + {"kernel: baz-linux\n", "", `"kernel" header is mandatory`}, + {"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`}, + {"store: brand-store\n", "store:\n - xyz\n", `"store" header must be a string`}, + {mods.tsLine, "", `"timestamp" header is mandatory`}, + {mods.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {mods.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + {reqSnaps, "required-snaps: foo\n", `"required-snaps" header must be a list of strings`}, + {reqSnaps, "required-snaps:\n -\n - nested\n", `"required-snaps" header must be a list of strings`}, + {sysUserAuths, "system-user-authority:\n a: 1\n", `"system-user-authority" header must be '\*' or a list of account ids`}, + {sysUserAuths, "system-user-authority:\n - 5_6\n", `"system-user-authority" header must be '\*' or a list of account ids`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr) + } +} + +func (mods *modelSuite) TestModelCheck(c *C) { + ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1))) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db) + + headers := ex.Headers() + headers["brand-id"] = brandDB.AuthorityID + headers["timestamp"] = time.Now().Format(time.RFC3339) + model, err := brandDB.Sign(asserts.ModelType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(model) + c.Assert(err, IsNil) +} + +func (mods *modelSuite) TestModelCheckInconsistentTimestamp(c *C) { + ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1))) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db) + + headers := ex.Headers() + headers["brand-id"] = brandDB.AuthorityID + headers["timestamp"] = "2011-01-01T14:00:00Z" + model, err := brandDB.Sign(asserts.ModelType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(model) + c.Assert(err, ErrorMatches, `model assertion timestamp outside of signing key validity \(key valid since.*\)`) +} + +func (mods *modelSuite) TestClassicDecodeOK(c *C) { + encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.AuthorityID(), Equals, "brand-id1") + c.Check(model.Timestamp(), Equals, mods.ts) + c.Check(model.Series(), Equals, "16") + c.Check(model.BrandID(), Equals, "brand-id1") + c.Check(model.Model(), Equals, "baz-3000") + c.Check(model.DisplayName(), Equals, "Baz 3000") + c.Check(model.Classic(), Equals, true) + c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.Kernel(), Equals, "") + c.Check(model.Store(), Equals, "brand-store") + c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) +} + +func (mods *modelSuite) TestClassicDecodeInvalid(c *C) { + encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"classic: true\n", "classic: foo\n", `"classic" header must be 'true' or 'false'`}, + {"architecture: amd64\n", "architecture:\n - foo\n", `"architecture" header must be a string`}, + {"gadget: brand-gadget\n", "gadget:\n - foo\n", `"gadget" header must be a string`}, + {"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a classic model`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr) + } +} + +func (mods *modelSuite) TestClassicDecodeGadgetAndArchOptional(c *C) { + encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "gadget: brand-gadget\n", "", 1) + encoded = strings.Replace(encoded, "architecture: amd64\n", "", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.Classic(), Equals, true) + c.Check(model.Architecture(), Equals, "") + c.Check(model.Gadget(), Equals, "") +} + +type serialSuite struct { + ts time.Time + tsLine string + deviceKey asserts.PrivateKey + encodedDevKey string +} + +func (ss *serialSuite) SetUpSuite(c *C) { + ss.ts = time.Now().Truncate(time.Second).UTC() + ss.tsLine = "timestamp: " + ss.ts.Format(time.RFC3339) + "\n" + + ss.deviceKey = testPrivKey2 + encodedPubKey, err := asserts.EncodePublicKey(ss.deviceKey.PublicKey()) + c.Assert(err, IsNil) + ss.encodedDevKey = string(encodedPubKey) +} + +const serialExample = "type: serial\n" + + "authority-id: brand-id1\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "serial: 2700\n" + + "device-key:\n DEVICEKEY\n" + + "device-key-sha3-384: KEYID\n" + + "TSLINE" + + "body-length: 2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "HW" + + "\n\n" + + "AXNpZw==" + +func (ss *serialSuite) TestDecodeOK(c *C) { + encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1) + encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + encoded = strings.Replace(encoded, "KEYID", ss.deviceKey.PublicKey().ID(), 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SerialType) + serial := a.(*asserts.Serial) + c.Check(serial.AuthorityID(), Equals, "brand-id1") + c.Check(serial.Timestamp(), Equals, ss.ts) + c.Check(serial.BrandID(), Equals, "brand-id1") + c.Check(serial.Model(), Equals, "baz-3000") + c.Check(serial.Serial(), Equals, "2700") + c.Check(serial.DeviceKey().ID(), Equals, ss.deviceKey.PublicKey().ID()) +} + +const ( + deviceSessReqErrPrefix = "assertion device-session-request: " + serialErrPrefix = "assertion serial: " + serialReqErrPrefix = "assertion serial-request: " +) + +func (ss *serialSuite) TestDecodeInvalid(c *C) { + encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`}, + {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"authority-id: brand-id1\n", "authority-id: random\n", `authority-id and brand-id must match, serial assertions are expected to be signed by the brand: "random" != "brand-id1"`}, + {"model: baz-3000\n", "", `"model" header is mandatory`}, + {"model: baz-3000\n", "model: \n", `"model" header should not be empty`}, + {"model: baz-3000\n", "model: _what\n", `"model" header contains invalid characters: "_what"`}, + {"serial: 2700\n", "", `"serial" header is mandatory`}, + {"serial: 2700\n", "serial: \n", `"serial" header should not be empty`}, + {ss.tsLine, "", `"timestamp" header is mandatory`}, + {ss.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {ss.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`}, + {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`}, + {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`}, + {"device-key-sha3-384: KEYID\n", "", `"device-key-sha3-384" header is mandatory`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + invalid = strings.Replace(invalid, "KEYID", ss.deviceKey.PublicKey().ID(), 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, serialErrPrefix+test.expectedErr) + } +} + +func (ss *serialSuite) TestDecodeKeyIDMismatch(c *C) { + invalid := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1) + invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + invalid = strings.Replace(invalid, "KEYID", "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", 1) + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, serialErrPrefix+"device key does not match provided key id") +} + +func (ss *serialSuite) TestSerialCheck(c *C) { + encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1) + encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + encoded = strings.Replace(encoded, "KEYID", ss.deviceKey.PublicKey().ID(), 1) + ex, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + storeDB, db := makeStoreAndCheckDB(c) + brandDB := setup3rdPartySigning(c, "brand1", storeDB, db) + + tests := []struct { + signDB assertstest.SignerDB + brandID string + authID string + keyID string + }{ + {brandDB, brandDB.AuthorityID, "", brandDB.KeyID}, + } + + for _, test := range tests { + headers := ex.Headers() + headers["brand-id"] = test.brandID + if test.authID != "" { + headers["authority-id"] = test.authID + } else { + headers["authority-id"] = test.brandID + } + headers["timestamp"] = time.Now().Format(time.RFC3339) + serial, err := test.signDB.Sign(asserts.SerialType, headers, nil, test.keyID) + c.Assert(err, IsNil) + + err = db.Check(serial) + c.Check(err, IsNil) + } +} + +func (ss *serialSuite) TestSerialRequestHappy(c *C) { + sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType, + map[string]interface{}{ + "brand-id": "brand-id1", + "model": "baz-3000", + "device-key": ss.encodedDevKey, + "request-id": "REQID", + }, []byte("HW-DETAILS"), ss.deviceKey) + c.Assert(err, IsNil) + + // roundtrip + a, err := asserts.Decode(asserts.Encode(sreq)) + c.Assert(err, IsNil) + + sreq2, ok := a.(*asserts.SerialRequest) + c.Assert(ok, Equals, true) + + // standalone signature check + err = asserts.SignatureCheck(sreq2, sreq2.DeviceKey()) + c.Check(err, IsNil) + + c.Check(sreq2.BrandID(), Equals, "brand-id1") + c.Check(sreq2.Model(), Equals, "baz-3000") + c.Check(sreq2.RequestID(), Equals, "REQID") + + c.Check(sreq2.Serial(), Equals, "") +} + +func (ss *serialSuite) TestSerialRequestHappyOptionalSerial(c *C) { + sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType, + map[string]interface{}{ + "brand-id": "brand-id1", + "model": "baz-3000", + "serial": "pserial", + "device-key": ss.encodedDevKey, + "request-id": "REQID", + }, []byte("HW-DETAILS"), ss.deviceKey) + c.Assert(err, IsNil) + + // roundtrip + a, err := asserts.Decode(asserts.Encode(sreq)) + c.Assert(err, IsNil) + + sreq2, ok := a.(*asserts.SerialRequest) + c.Assert(ok, Equals, true) + + c.Check(sreq2.Model(), Equals, "baz-3000") + c.Check(sreq2.Serial(), Equals, "pserial") +} + +func (ss *serialSuite) TestSerialRequestDecodeInvalid(c *C) { + encoded := "type: serial-request\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "device-key:\n DEVICEKEY\n" + + "request-id: REQID\n" + + "serial: S\n" + + "body-length: 2\n" + + "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" + + "HW" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`}, + {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"model: baz-3000\n", "", `"model" header is mandatory`}, + {"model: baz-3000\n", "model: \n", `"model" header should not be empty`}, + {"request-id: REQID\n", "", `"request-id" header is mandatory`}, + {"request-id: REQID\n", "request-id: \n", `"request-id" header should not be empty`}, + {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`}, + {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`}, + {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`}, + {"serial: S\n", "serial:\n - xyz\n", `"serial" header must be a string`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1) + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, serialReqErrPrefix+test.expectedErr) + } +} + +func (ss *serialSuite) TestSerialRequestDecodeKeyIDMismatch(c *C) { + invalid := "type: serial-request\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "device-key:\n " + strings.Replace(ss.encodedDevKey, "\n", "\n ", -1) + "\n" + + "request-id: REQID\n" + + "body-length: 2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "HW" + + "\n\n" + + "AXNpZw==" + + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, "assertion serial-request: device key does not match included signing key id") +} + +func (ss *serialSuite) TestDeviceSessionRequest(c *C) { + ts := time.Now().UTC().Round(time.Second) + sessReq, err := asserts.SignWithoutAuthority(asserts.DeviceSessionRequestType, + map[string]interface{}{ + "brand-id": "brand-id1", + "model": "baz-3000", + "serial": "99990", + "nonce": "NONCE", + "timestamp": ts.Format(time.RFC3339), + }, nil, ss.deviceKey) + c.Assert(err, IsNil) + + // roundtrip + a, err := asserts.Decode(asserts.Encode(sessReq)) + c.Assert(err, IsNil) + + sessReq2, ok := a.(*asserts.DeviceSessionRequest) + c.Assert(ok, Equals, true) + + // standalone signature check + err = asserts.SignatureCheck(sessReq2, ss.deviceKey.PublicKey()) + c.Check(err, IsNil) + + c.Check(sessReq2.BrandID(), Equals, "brand-id1") + c.Check(sessReq2.Model(), Equals, "baz-3000") + c.Check(sessReq2.Serial(), Equals, "99990") + c.Check(sessReq2.Nonce(), Equals, "NONCE") + c.Check(sessReq2.Timestamp().Equal(ts), Equals, true) +} + +func (ss *serialSuite) TestDeviceSessionRequestDecodeInvalid(c *C) { + tsLine := "timestamp: " + time.Now().Format(time.RFC3339) + "\n" + encoded := "type: device-session-request\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "serial: 99990\n" + + "nonce: NONCE\n" + + tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"model: baz-3000\n", "model: \n", `"model" header should not be empty`}, + {"serial: 99990\n", "", `"serial" header is mandatory`}, + {"nonce: NONCE\n", "nonce: \n", `"nonce" header should not be empty`}, + {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, deviceSessReqErrPrefix+test.expectedErr) + } +} diff --git a/asserts/digest.go b/asserts/digest.go new file mode 100644 index 00000000..6578772e --- /dev/null +++ b/asserts/digest.go @@ -0,0 +1,43 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "crypto" + "encoding/base64" + "fmt" +) + +// EncodeDigest encodes the digest from hash algorithm to be put in an assertion header. +func EncodeDigest(hash crypto.Hash, hashDigest []byte) (string, error) { + algo := "" + switch hash { + case crypto.SHA512: + algo = "sha512" + case crypto.SHA3_384: + algo = "sha3-384" + default: + return "", fmt.Errorf("unsupported hash") + } + if len(hashDigest) != hash.Size() { + return "", fmt.Errorf("hash digest by %s should be %d bytes", algo, hash.Size()) + } + return base64.RawURLEncoding.EncodeToString(hashDigest), nil +} diff --git a/asserts/digest_test.go b/asserts/digest_test.go new file mode 100644 index 00000000..a9406925 --- /dev/null +++ b/asserts/digest_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "crypto" + _ "crypto/sha256" + "encoding/base64" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type encodeDigestSuite struct{} + +var _ = Suite(&encodeDigestSuite{}) + +func (eds *encodeDigestSuite) TestEncodeDigestOK(c *C) { + h := crypto.SHA512.New() + h.Write([]byte("some stuff to hash")) + digest := h.Sum(nil) + encoded, err := asserts.EncodeDigest(crypto.SHA512, digest) + c.Assert(err, IsNil) + + decoded, err := base64.RawURLEncoding.DecodeString(encoded) + c.Assert(err, IsNil) + c.Check(decoded, DeepEquals, digest) + + // sha3-384 + b, err := base64.RawURLEncoding.DecodeString(blobSHA3_384) + c.Assert(err, IsNil) + encoded, err = asserts.EncodeDigest(crypto.SHA3_384, b) + c.Assert(err, IsNil) + c.Check(encoded, Equals, blobSHA3_384) + +} + +func (eds *encodeDigestSuite) TestEncodeDigestErrors(c *C) { + _, err := asserts.EncodeDigest(crypto.SHA1, nil) + c.Check(err, ErrorMatches, "unsupported hash") + + _, err = asserts.EncodeDigest(crypto.SHA512, []byte{1, 2}) + c.Check(err, ErrorMatches, "hash digest by sha512 should be 64 bytes") + + _, err = asserts.EncodeDigest(crypto.SHA3_384, []byte{1, 2}) + c.Check(err, ErrorMatches, "hash digest by sha3-384 should be 48 bytes") +} diff --git a/asserts/export_test.go b/asserts/export_test.go new file mode 100644 index 00000000..1f1bee1b --- /dev/null +++ b/asserts/export_test.go @@ -0,0 +1,193 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "io" + "time" +) + +// expose test-only things here + +var NumAssertionType = len(typeRegistry) + +// v1FixedTimestamp exposed for tests +var V1FixedTimestamp = v1FixedTimestamp + +// assembleAndSign exposed for tests +var AssembleAndSignInTest = assembleAndSign + +// decodePrivateKey exposed for tests +var DecodePrivateKeyInTest = decodePrivateKey + +// NewDecoderStressed makes a Decoder with a stressed setup with the given buffer and maximum sizes. +func NewDecoderStressed(r io.Reader, bufSize, maxHeadersSize, maxBodySize, maxSigSize int) *Decoder { + return (&Decoder{ + rd: r, + initialBufSize: bufSize, + maxHeadersSize: maxHeadersSize, + maxSigSize: maxSigSize, + defaultMaxBodySize: maxBodySize, + }).initBuffer() +} + +func BootstrapAccountForTest(authorityID string) *Account { + return &Account{ + assertionBase: assertionBase{ + headers: map[string]interface{}{ + "type": "account", + "authority-id": authorityID, + "account-id": authorityID, + "validation": "certified", + }, + }, + timestamp: time.Now().UTC(), + } +} + +func makeAccountKeyForTest(authorityID string, openPGPPubKey PublicKey, validYears int) *AccountKey { + return &AccountKey{ + assertionBase: assertionBase{ + headers: map[string]interface{}{ + "type": "account-key", + "authority-id": authorityID, + "account-id": authorityID, + "public-key-sha3-384": openPGPPubKey.ID(), + }, + }, + since: time.Time{}, + until: time.Time{}.UTC().AddDate(validYears, 0, 0), + pubKey: openPGPPubKey, + } +} + +func BootstrapAccountKeyForTest(authorityID string, pubKey PublicKey) *AccountKey { + return makeAccountKeyForTest(authorityID, pubKey, 9999) +} + +func ExpiredAccountKeyForTest(authorityID string, pubKey PublicKey) *AccountKey { + return makeAccountKeyForTest(authorityID, pubKey, 1) +} + +// define dummy assertion types to use in the tests + +type TestOnly struct { + assertionBase +} + +func assembleTestOnly(assert assertionBase) (Assertion, error) { + // for testing error cases + if _, err := checkIntWithDefault(assert.headers, "count", 0); err != nil { + return nil, err + } + return &TestOnly{assert}, nil +} + +var TestOnlyType = &AssertionType{"test-only", []string{"primary-key"}, assembleTestOnly, 0} + +type TestOnly2 struct { + assertionBase +} + +func assembleTestOnly2(assert assertionBase) (Assertion, error) { + return &TestOnly2{assert}, nil +} + +var TestOnly2Type = &AssertionType{"test-only-2", []string{"pk1", "pk2"}, assembleTestOnly2, 0} + +type TestOnlyNoAuthority struct { + assertionBase +} + +func assembleTestOnlyNoAuthority(assert assertionBase) (Assertion, error) { + if _, err := checkNotEmptyString(assert.headers, "hdr"); err != nil { + return nil, err + } + return &TestOnlyNoAuthority{assert}, nil +} + +var TestOnlyNoAuthorityType = &AssertionType{"test-only-no-authority", nil, assembleTestOnlyNoAuthority, noAuthority} + +type TestOnlyNoAuthorityPK struct { + assertionBase +} + +func assembleTestOnlyNoAuthorityPK(assert assertionBase) (Assertion, error) { + return &TestOnlyNoAuthorityPK{assert}, nil +} + +var TestOnlyNoAuthorityPKType = &AssertionType{"test-only-no-authority-pk", []string{"pk"}, assembleTestOnlyNoAuthorityPK, noAuthority} + +func init() { + typeRegistry[TestOnlyType.Name] = TestOnlyType + maxSupportedFormat[TestOnlyType.Name] = 1 + typeRegistry[TestOnly2Type.Name] = TestOnly2Type + typeRegistry[TestOnlyNoAuthorityType.Name] = TestOnlyNoAuthorityType + typeRegistry[TestOnlyNoAuthorityPKType.Name] = TestOnlyNoAuthorityPKType + formatAnalyzer[TestOnlyType] = func(headers map[string]interface{}, _ []byte) (int, error) { + if _, ok := headers["format-1-feature"]; ok { + return 1, nil + } + return 0, nil + } +} + +// AccountKeyIsKeyValidAt exposes isKeyValidAt on AccountKey for tests +func AccountKeyIsKeyValidAt(ak *AccountKey, when time.Time) bool { + return ak.isKeyValidAt(when) +} + +type GPGRunner func(input []byte, args ...string) ([]byte, error) + +func MockRunGPG(mock func(prev GPGRunner, input []byte, args ...string) ([]byte, error)) (restore func()) { + prevRunGPG := runGPG + runGPG = func(input []byte, args ...string) ([]byte, error) { + return mock(prevRunGPG, input, args...) + } + return func() { + runGPG = prevRunGPG + } +} + +// Headers helpers to test +var ( + ParseHeaders = parseHeaders + AppendEntry = appendEntry +) + +// ParametersForGenerate exposes parametersForGenerate for tests. +func (gkm *GPGKeypairManager) ParametersForGenerate(passphrase string, name string) string { + return gkm.parametersForGenerate(passphrase, name) +} + +// ifacedecls tests +var ( + CompileAttributeConstraints = compileAttributeConstraints + CompilePlugRule = compilePlugRule + CompileSlotRule = compileSlotRule +) + +type featureExposer interface { + feature(flabel string) bool +} + +func RuleFeature(rule featureExposer, flabel string) bool { + return rule.feature(flabel) +} diff --git a/asserts/fetcher.go b/asserts/fetcher.go new file mode 100644 index 00000000..0e353e35 --- /dev/null +++ b/asserts/fetcher.go @@ -0,0 +1,121 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" +) + +type fetchProgress int + +const ( + fetchNotSeen fetchProgress = iota + fetchRetrieved + fetchSaved +) + +// A Fetcher helps fetching assertions and their prerequisites. +type Fetcher interface { + // Fetch retrieves the assertion indicated by ref then its prerequisites + // recursively, along the way saving prerequisites before dependent assertions. + Fetch(*Ref) error + // Save retrieves the prerequisites of the assertion recursively, + // along the way saving them, and finally saves the assertion. + Save(Assertion) error +} + +type fetcher struct { + db RODatabase + retrieve func(*Ref) (Assertion, error) + save func(Assertion) error + + fetched map[string]fetchProgress +} + +// NewFetcher creates a Fetcher which will use trustedDB to determine trusted assertions, will fetch assertions following prerequisites using retrieve, and then will pass them to save, saving prerequisites before dependent assertions. +func NewFetcher(trustedDB RODatabase, retrieve func(*Ref) (Assertion, error), save func(Assertion) error) Fetcher { + return &fetcher{ + db: trustedDB, + retrieve: retrieve, + save: save, + fetched: make(map[string]fetchProgress), + } +} + +func (f *fetcher) chase(ref *Ref, a Assertion) error { + // check if ref points to predefined assertion, in which case + // there is nothing to do + _, err := ref.Resolve(f.db.FindPredefined) + if err == nil { + return nil + } + if !IsNotFound(err) { + return err + } + u := ref.Unique() + switch f.fetched[u] { + case fetchSaved: + return nil // nothing to do + case fetchRetrieved: + return fmt.Errorf("circular assertions are not expected: %s", ref) + } + if a == nil { + retrieved, err := f.retrieve(ref) + if err != nil { + return err + } + a = retrieved + } + f.fetched[u] = fetchRetrieved + for _, preref := range a.Prerequisites() { + if err := f.Fetch(preref); err != nil { + return err + } + } + if err := f.fetchAccountKey(a.SignKeyID()); err != nil { + return err + } + if err := f.save(a); err != nil { + return err + } + f.fetched[u] = fetchSaved + return nil +} + +// Fetch retrieves the assertion indicated by ref then its prerequisites +// recursively, along the way saving prerequisites before dependent assertions. +func (f *fetcher) Fetch(ref *Ref) error { + return f.chase(ref, nil) +} + +// fetchAccountKey behaves like Fetch for the account-key with the given key id. +func (f *fetcher) fetchAccountKey(keyID string) error { + keyRef := &Ref{ + Type: AccountKeyType, + PrimaryKey: []string{keyID}, + } + return f.Fetch(keyRef) +} + +// Save retrieves the prerequisites of the assertion recursively, +// along the way saving them, and finally saves the assertion. +func (f *fetcher) Save(a Assertion) error { + return f.chase(a.Ref(), a) +} diff --git a/asserts/fetcher_test.go b/asserts/fetcher_test.go new file mode 100644 index 00000000..36531e75 --- /dev/null +++ b/asserts/fetcher_test.go @@ -0,0 +1,167 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "crypto" + "fmt" + "time" + + "golang.org/x/crypto/sha3" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +type fetcherSuite struct { + storeSigning *assertstest.StoreStack +} + +var _ = Suite(&fetcherSuite{}) + +func (s *fetcherSuite) SetUpTest(c *C) { + s.storeSigning = assertstest.NewStoreStack("can0nical", nil) +} + +func fakeSnap(rev int) []byte { + fake := fmt.Sprintf("hsqs________________%d", rev) + return []byte(fake) +} + +func fakeHash(rev int) []byte { + h := sha3.Sum384(fakeSnap(rev)) + return h[:] +} + +func makeDigest(rev int) string { + d, err := asserts.EncodeDigest(crypto.SHA3_384, fakeHash(rev)) + if err != nil { + panic(err) + } + return string(d) +} + +func (s *fetcherSuite) prereqSnapAssertions(c *C, revisions ...int) { + dev1Acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + err := s.storeSigning.Add(dev1Acct) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapDecl) + c.Assert(err, IsNil) + + for _, rev := range revisions { + headers = map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-sha3-384": makeDigest(rev), + "snap-size": "1000", + "snap-revision": fmt.Sprintf("%d", rev), + "developer-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapRev) + c.Assert(err, IsNil) + } +} + +func (s *fetcherSuite) TestFetch(c *C) { + s.prereqSnapAssertions(c, 10) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.storeSigning.Trusted, + }) + c.Assert(err, IsNil) + + ref := &asserts.Ref{ + Type: asserts.SnapRevisionType, + PrimaryKey: []string{makeDigest(10)}, + } + + retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { + return ref.Resolve(s.storeSigning.Find) + } + + f := asserts.NewFetcher(db, retrieve, db.Add) + + err = f.Fetch(ref) + c.Assert(err, IsNil) + + snapRev, err := ref.Resolve(db.Find) + c.Assert(err, IsNil) + c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10) + + snapDecl, err := db.Find(asserts.SnapDeclarationType, map[string]string{ + "series": "16", + "snap-id": "snap-id-1", + }) + c.Assert(err, IsNil) + c.Check(snapDecl.(*asserts.SnapDeclaration).SnapName(), Equals, "foo") +} + +func (s *fetcherSuite) TestSave(c *C) { + s.prereqSnapAssertions(c, 10) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.storeSigning.Trusted, + }) + c.Assert(err, IsNil) + + retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { + return ref.Resolve(s.storeSigning.Find) + } + + f := asserts.NewFetcher(db, retrieve, db.Add) + + ref := &asserts.Ref{ + Type: asserts.SnapRevisionType, + PrimaryKey: []string{makeDigest(10)}, + } + rev, err := ref.Resolve(s.storeSigning.Find) + c.Assert(err, IsNil) + + err = f.Save(rev) + c.Assert(err, IsNil) + + snapRev, err := ref.Resolve(db.Find) + c.Assert(err, IsNil) + c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10) + + snapDecl, err := db.Find(asserts.SnapDeclarationType, map[string]string{ + "series": "16", + "snap-id": "snap-id-1", + }) + c.Assert(err, IsNil) + c.Check(snapDecl.(*asserts.SnapDeclaration).SnapName(), Equals, "foo") +} diff --git a/asserts/findwildcard.go b/asserts/findwildcard.go new file mode 100644 index 00000000..abac11bf --- /dev/null +++ b/asserts/findwildcard.go @@ -0,0 +1,111 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +/* +findWildcard invokes foundCb once for each parent directory of regular files matching: + +//... + +where each descendantWithWildcard component can contain the * wildcard; + +foundCb is invoked with the paths of the found regular files relative to top (that means top/ is excluded). + +Unlike filepath.Glob any I/O operation error stops the walking and bottoms out, so does a foundCb invocation that returns an error. +*/ +func findWildcard(top string, descendantWithWildcard []string, foundCb func(relpath []string) error) error { + return findWildcardDescend(top, top, descendantWithWildcard, foundCb) +} + +func findWildcardBottom(top, current string, pat string, names []string, foundCb func(relpath []string) error) error { + var hits []string + for _, name := range names { + ok, err := filepath.Match(pat, name) + if err != nil { + return fmt.Errorf("findWildcard: invoked with malformed wildcard: %v", err) + } + if !ok { + continue + } + fn := filepath.Join(current, name) + finfo, err := os.Stat(fn) + if os.IsNotExist(err) { + continue + } + if err != nil { + return err + } + if !finfo.Mode().IsRegular() { + return fmt.Errorf("expected a regular file: %v", fn) + } + relpath, err := filepath.Rel(top, fn) + if err != nil { + return fmt.Errorf("findWildcard: unexpected to fail at computing rel path of descendant") + } + hits = append(hits, relpath) + } + if len(hits) == 0 { + return nil + } + return foundCb(hits) +} + +func findWildcardDescend(top, current string, descendantWithWildcard []string, foundCb func(relpath []string) error) error { + k := descendantWithWildcard[0] + if len(descendantWithWildcard) > 1 && strings.IndexByte(k, '*') == -1 { + return findWildcardDescend(top, filepath.Join(current, k), descendantWithWildcard[1:], foundCb) + } + + d, err := os.Open(current) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + defer d.Close() + names, err := d.Readdirnames(-1) + if err != nil { + return err + } + if len(descendantWithWildcard) == 1 { + return findWildcardBottom(top, current, k, names, foundCb) + } + for _, name := range names { + ok, err := filepath.Match(k, name) + if err != nil { + return fmt.Errorf("findWildcard: invoked with malformed wildcard: %v", err) + } + if ok { + err = findWildcardDescend(top, filepath.Join(current, name), descendantWithWildcard[1:], foundCb) + if err != nil { + return err + } + } + } + return nil +} diff --git a/asserts/findwildcard_test.go b/asserts/findwildcard_test.go new file mode 100644 index 00000000..f5094ca7 --- /dev/null +++ b/asserts/findwildcard_test.go @@ -0,0 +1,139 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" + "sort" + + "gopkg.in/check.v1" +) + +type findWildcardSuite struct{} + +var _ = check.Suite(&findWildcardSuite{}) + +func (fs *findWildcardSuite) TestFindWildcard(c *check.C) { + top := filepath.Join(c.MkDir(), "top") + + err := os.MkdirAll(top, os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id1"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id1", "abcd"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id1", "e5cd"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id2"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id2", "f444"), os.ModePerm) + c.Assert(err, check.IsNil) + + err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd", "active"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd", "active.1"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "e5cd", "active"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(top, "acc-id2", "f444", "active"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + + var res []string + foundCb := func(relpath []string) error { + res = append(res, relpath...) + return nil + } + + err = findWildcard(top, []string{"*", "*", "active"}, foundCb) + c.Assert(err, check.IsNil) + sort.Strings(res) + c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/e5cd/active", "acc-id2/f444/active"}) + + res = nil + err = findWildcard(top, []string{"*", "*", "active*"}, foundCb) + c.Assert(err, check.IsNil) + sort.Strings(res) + c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/abcd/active.1", "acc-id1/e5cd/active", "acc-id2/f444/active"}) + + res = nil + err = findWildcard(top, []string{"zoo", "*", "active"}, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) + + res = nil + err = findWildcard(top, []string{"zoo", "*", "active*"}, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) + + res = nil + err = findWildcard(top, []string{"a*", "zoo", "active"}, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) + + res = nil + err = findWildcard(top, []string{"acc-id1", "*cd", "active"}, foundCb) + c.Assert(err, check.IsNil) + sort.Strings(res) + c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/e5cd/active"}) + + res = nil + err = findWildcard(top, []string{"acc-id1", "*cd", "active*"}, foundCb) + c.Assert(err, check.IsNil) + sort.Strings(res) + c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/abcd/active.1", "acc-id1/e5cd/active"}) + +} + +func (fs *findWildcardSuite) TestFindWildcardSomeErrors(c *check.C) { + top := filepath.Join(c.MkDir(), "top-errors") + + err := os.MkdirAll(top, os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id1"), os.ModePerm) + c.Assert(err, check.IsNil) + err = os.MkdirAll(filepath.Join(top, "acc-id2"), os.ModePerm) + c.Assert(err, check.IsNil) + + err = ioutil.WriteFile(filepath.Join(top, "acc-id1", "abcd"), nil, os.ModePerm) + c.Assert(err, check.IsNil) + + err = os.MkdirAll(filepath.Join(top, "acc-id2", "dddd"), os.ModePerm) + c.Assert(err, check.IsNil) + + var res []string + var retErr error + foundCb := func(relpath []string) error { + res = append(res, relpath...) + return retErr + } + + myErr := errors.New("boom") + retErr = myErr + err = findWildcard(top, []string{"acc-id1", "*"}, foundCb) + c.Check(err, check.Equals, myErr) + + retErr = nil + res = nil + err = findWildcard(top, []string{"acc-id2", "*"}, foundCb) + c.Check(err, check.ErrorMatches, "expected a regular file: .*") +} diff --git a/asserts/fsbackstore.go b/asserts/fsbackstore.go new file mode 100644 index 00000000..632079a9 --- /dev/null +++ b/asserts/fsbackstore.go @@ -0,0 +1,221 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "sync" +) + +// the default filesystem based backstore for assertions + +const ( + assertionsLayoutVersion = "v0" + assertionsRoot = "asserts-" + assertionsLayoutVersion +) + +type filesystemBackstore struct { + top string + mu sync.RWMutex +} + +// OpenFSBackstore opens a filesystem backed assertions backstore under path. +func OpenFSBackstore(path string) (Backstore, error) { + top := filepath.Join(path, assertionsRoot) + err := ensureTop(top) + if err != nil { + return nil, err + } + return &filesystemBackstore{top: top}, nil +} + +// guarantees that result assertion is of the expected type (both in the AssertionType and go type sense) +func (fsbs *filesystemBackstore) readAssertion(assertType *AssertionType, diskPrimaryPath string) (Assertion, error) { + encoded, err := readEntry(fsbs.top, assertType.Name, diskPrimaryPath) + if os.IsNotExist(err) { + return nil, errNotFound + } + if err != nil { + return nil, fmt.Errorf("broken assertion storage, cannot read assertion: %v", err) + } + assert, err := Decode(encoded) + if err != nil { + return nil, fmt.Errorf("broken assertion storage, cannot decode assertion: %v", err) + } + if assert.Type() != assertType { + return nil, fmt.Errorf("assertion that is not of type %q under their storage tree", assertType.Name) + } + // because of Decode() construction assert has also the expected go type + return assert, nil +} + +func (fsbs *filesystemBackstore) pickLatestAssertion(assertType *AssertionType, diskPrimaryPaths []string, maxFormat int) (a Assertion, er error) { + for _, diskPrimaryPath := range diskPrimaryPaths { + fn := filepath.Base(diskPrimaryPath) + parts := strings.SplitN(fn, ".", 2) + formatnum := 0 + if len(parts) == 2 { + var err error + formatnum, err = strconv.Atoi(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid active assertion filename: %q", fn) + } + } + if formatnum <= maxFormat { + a1, err := fsbs.readAssertion(assertType, diskPrimaryPath) + if err != nil { + return nil, err + } + if a == nil || a1.Revision() > a.Revision() { + a = a1 + } + } + } + if a == nil { + return nil, errNotFound + } + return a, nil +} + +func diskPrimaryPathComps(primaryPath []string, active string) []string { + n := len(primaryPath) + comps := make([]string, n+1) + // safety against '/' etc + for i, comp := range primaryPath { + comps[i] = url.QueryEscape(comp) + } + comps[n] = active + return comps +} + +func (fsbs *filesystemBackstore) currentAssertion(assertType *AssertionType, primaryPath []string, maxFormat int) (Assertion, error) { + var a Assertion + namesCb := func(relpaths []string) error { + var err error + a, err = fsbs.pickLatestAssertion(assertType, relpaths, maxFormat) + if err == errNotFound { + return nil + } + return err + } + + comps := diskPrimaryPathComps(primaryPath, "active*") + assertTypeTop := filepath.Join(fsbs.top, assertType.Name) + err := findWildcard(assertTypeTop, comps, namesCb) + if err != nil { + return nil, fmt.Errorf("broken assertion storage, looking for %s: %v", assertType.Name, err) + } + + if a == nil { + return nil, errNotFound + } + + return a, nil +} + +func (fsbs *filesystemBackstore) Put(assertType *AssertionType, assert Assertion) error { + fsbs.mu.Lock() + defer fsbs.mu.Unlock() + + primaryPath := assert.Ref().PrimaryKey + + curAssert, err := fsbs.currentAssertion(assertType, primaryPath, assertType.MaxSupportedFormat()) + if err == nil { + curRev := curAssert.Revision() + rev := assert.Revision() + if curRev >= rev { + return &RevisionError{Current: curRev, Used: rev} + } + } else if err != errNotFound { + return err + } + + formatnum := assert.Format() + activeFn := "active" + if formatnum > 0 { + activeFn = fmt.Sprintf("active.%d", formatnum) + } + diskPrimaryPath := filepath.Join(diskPrimaryPathComps(primaryPath, activeFn)...) + err = atomicWriteEntry(Encode(assert), false, fsbs.top, assertType.Name, diskPrimaryPath) + if err != nil { + return fmt.Errorf("broken assertion storage, cannot write assertion: %v", err) + } + return nil +} + +func (fsbs *filesystemBackstore) Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) { + fsbs.mu.RLock() + defer fsbs.mu.RUnlock() + + a, err := fsbs.currentAssertion(assertType, key, maxFormat) + if err == errNotFound { + return nil, &NotFoundError{Type: assertType} + } + return a, err +} + +func (fsbs *filesystemBackstore) search(assertType *AssertionType, diskPattern []string, foundCb func(Assertion), maxFormat int) error { + assertTypeTop := filepath.Join(fsbs.top, assertType.Name) + candCb := func(diskPrimaryPaths []string) error { + a, err := fsbs.pickLatestAssertion(assertType, diskPrimaryPaths, maxFormat) + if err == errNotFound { + return nil + } + if err != nil { + return err + } + foundCb(a) + return nil + } + err := findWildcard(assertTypeTop, diskPattern, candCb) + if err != nil { + return fmt.Errorf("broken assertion storage, searching for %s: %v", assertType.Name, err) + } + return nil +} + +func (fsbs *filesystemBackstore) Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error { + fsbs.mu.RLock() + defer fsbs.mu.RUnlock() + + n := len(assertType.PrimaryKey) + diskPattern := make([]string, n+1) + for i, k := range assertType.PrimaryKey { + keyVal := headers[k] + if keyVal == "" { + diskPattern[i] = "*" + } else { + diskPattern[i] = url.QueryEscape(keyVal) + } + } + diskPattern[n] = "active*" + + candCb := func(a Assertion) { + if searchMatch(a, headers) { + foundCb(a) + } + } + return fsbs.search(assertType, diskPattern, candCb, maxFormat) +} diff --git a/asserts/fsbackstore_test.go b/asserts/fsbackstore_test.go new file mode 100644 index 00000000..003d9983 --- /dev/null +++ b/asserts/fsbackstore_test.go @@ -0,0 +1,258 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "os" + "path/filepath" + "syscall" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type fsBackstoreSuite struct{} + +var _ = Suite(&fsBackstoreSuite{}) + +func (fsbss *fsBackstoreSuite) TestOpenOK(c *C) { + // ensure umask is clean when creating the DB dir + oldUmask := syscall.Umask(0) + defer syscall.Umask(oldUmask) + + topDir := filepath.Join(c.MkDir(), "asserts-db") + + bs, err := asserts.OpenFSBackstore(topDir) + c.Check(err, IsNil) + c.Check(bs, NotNil) + + info, err := os.Stat(filepath.Join(topDir, "asserts-v0")) + c.Assert(err, IsNil) + c.Assert(info.IsDir(), Equals, true) + c.Check(info.Mode().Perm(), Equals, os.FileMode(0775)) +} + +func (fsbss *fsBackstoreSuite) TestOpenCreateFail(c *C) { + parent := filepath.Join(c.MkDir(), "var") + topDir := filepath.Join(parent, "asserts-db") + // make it not writable + err := os.Mkdir(parent, 0555) + c.Assert(err, IsNil) + + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, ErrorMatches, "cannot create assert storage root: .*") + c.Check(bs, IsNil) +} + +func (fsbss *fsBackstoreSuite) TestOpenWorldWritableFail(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + // make it world-writable + oldUmask := syscall.Umask(0) + os.MkdirAll(filepath.Join(topDir, "asserts-v0"), 0777) + syscall.Umask(oldUmask) + + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") + c.Check(bs, IsNil) +} + +func (fsbss *fsBackstoreSuite) TestPutOldRevision(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + // Create two revisions of assertion. + a0, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + a1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + // Put newer revision, follwed by old revision. + err = bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a0) + + c.Check(err, ErrorMatches, `revision 0 is older than current revision 1`) + c.Check(err, DeepEquals, &asserts.RevisionError{Current: 1, Used: 0}) +} + +func (fsbss *fsBackstoreSuite) TestGetFormat(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + af0, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "format: 1\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af2, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: zoo\n" + + "format: 2\n" + + "revision: 22\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + err = bs.Put(asserts.TestOnlyType, af0) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, af1) + c.Assert(err, IsNil) + + a, err := bs.Get(asserts.TestOnlyType, []string{"foo"}, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + a, err = bs.Get(asserts.TestOnlyType, []string{"foo"}, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 0) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + // Headers can be omitted by Backstores + }) + c.Check(a, IsNil) + + err = bs.Put(asserts.TestOnlyType, af2) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) + c.Check(a, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 2) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 22) +} + +func (fsbss *fsBackstoreSuite) TestSearchFormat(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + af0, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: bar\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af1, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: bar\n" + + "format: 1\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + af2, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: baz\n" + + "format: 2\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + err = bs.Put(asserts.TestOnly2Type, af0) + c.Assert(err, IsNil) + + queries := []map[string]string{ + {"pk1": "foo", "pk2": "bar"}, + {"pk1": "foo"}, + {"pk2": "bar"}, + } + + for _, q := range queries { + var a asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + a = a1 + } + err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + } + + err = bs.Put(asserts.TestOnly2Type, af1) + c.Assert(err, IsNil) + + for _, q := range queries { + var a asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + a = a1 + } + err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + err = bs.Search(asserts.TestOnly2Type, q, foundCb, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + } + + err = bs.Put(asserts.TestOnly2Type, af2) + c.Assert(err, IsNil) + + var as []asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + as = append(as, a1) + } + err = bs.Search(asserts.TestOnly2Type, map[string]string{ + "pk1": "foo", + }, foundCb, 1) // will not find af2 + c.Assert(err, IsNil) + c.Check(as, HasLen, 1) + c.Check(as[0].Revision(), Equals, 1) + +} diff --git a/asserts/fsentryutils.go b/asserts/fsentryutils.go new file mode 100644 index 00000000..ca057d8c --- /dev/null +++ b/asserts/fsentryutils.go @@ -0,0 +1,70 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/snapcore/snapd/osutil" +) + +// utilities to read/write fs entries + +func ensureTop(path string) error { + err := os.MkdirAll(path, 0775) + if err != nil { + return fmt.Errorf("cannot create assert storage root: %v", err) + } + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("cannot create assert storage root: %v", err) + } + if info.Mode().Perm()&0002 != 0 { + return fmt.Errorf("assert storage root unexpectedly world-writable: %v", path) + } + return nil +} + +func atomicWriteEntry(data []byte, secret bool, top string, subpath ...string) error { + fpath := filepath.Join(top, filepath.Join(subpath...)) + dir := filepath.Dir(fpath) + err := os.MkdirAll(dir, 0775) + if err != nil { + return err + } + fperm := 0664 + if secret { + fperm = 0600 + } + return osutil.AtomicWriteFile(fpath, data, os.FileMode(fperm), 0) +} + +func entryExists(top string, subpath ...string) bool { + fpath := filepath.Join(top, filepath.Join(subpath...)) + return osutil.FileExists(fpath) +} + +func readEntry(top string, subpath ...string) ([]byte, error) { + fpath := filepath.Join(top, filepath.Join(subpath...)) + return ioutil.ReadFile(fpath) +} diff --git a/asserts/fskeypairmgr.go b/asserts/fskeypairmgr.go new file mode 100644 index 00000000..5a58ae17 --- /dev/null +++ b/asserts/fskeypairmgr.go @@ -0,0 +1,92 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sync" +) + +// the default simple filesystem based keypair manager/backstore + +const ( + privateKeysLayoutVersion = "v1" + privateKeysRoot = "private-keys-" + privateKeysLayoutVersion +) + +type filesystemKeypairManager struct { + top string + mu sync.RWMutex +} + +// OpenFSKeypairManager opens a filesystem backed assertions backstore under path. +func OpenFSKeypairManager(path string) (KeypairManager, error) { + top := filepath.Join(path, privateKeysRoot) + err := ensureTop(top) + if err != nil { + return nil, err + } + return &filesystemKeypairManager{top: top}, nil +} + +var errKeypairAlreadyExists = errors.New("key pair with given key id already exists") + +func (fskm *filesystemKeypairManager) Put(privKey PrivateKey) error { + keyID := privKey.PublicKey().ID() + if entryExists(fskm.top, keyID) { + return errKeypairAlreadyExists + } + encoded, err := encodePrivateKey(privKey) + if err != nil { + return fmt.Errorf("cannot store private key: %v", err) + } + + fskm.mu.Lock() + defer fskm.mu.Unlock() + + err = atomicWriteEntry(encoded, true, fskm.top, keyID) + if err != nil { + return fmt.Errorf("cannot store private key: %v", err) + } + return nil +} + +var errKeypairNotFound = errors.New("cannot find key pair") + +func (fskm *filesystemKeypairManager) Get(keyID string) (PrivateKey, error) { + fskm.mu.RLock() + defer fskm.mu.RUnlock() + + encoded, err := readEntry(fskm.top, keyID) + if os.IsNotExist(err) { + return nil, errKeypairNotFound + } + if err != nil { + return nil, fmt.Errorf("cannot read key pair: %v", err) + } + privKey, err := decodePrivateKey(encoded) + if err != nil { + return nil, fmt.Errorf("cannot decode key pair: %v", err) + } + return privKey, nil +} diff --git a/asserts/fskeypairmgr_test.go b/asserts/fskeypairmgr_test.go new file mode 100644 index 00000000..422ccdde --- /dev/null +++ b/asserts/fskeypairmgr_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "os" + "path/filepath" + "syscall" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type fsKeypairMgrSuite struct{} + +var _ = Suite(&fsKeypairMgrSuite{}) + +func (fsbss *fsKeypairMgrSuite) TestOpenOK(c *C) { + // ensure umask is clean when creating the DB dir + oldUmask := syscall.Umask(0) + defer syscall.Umask(oldUmask) + + topDir := filepath.Join(c.MkDir(), "asserts-db") + err := os.MkdirAll(topDir, 0775) + c.Assert(err, IsNil) + + bs, err := asserts.OpenFSKeypairManager(topDir) + c.Check(err, IsNil) + c.Check(bs, NotNil) + + info, err := os.Stat(filepath.Join(topDir, "private-keys-v1")) + c.Assert(err, IsNil) + c.Assert(info.IsDir(), Equals, true) + c.Check(info.Mode().Perm(), Equals, os.FileMode(0775)) +} + +func (fsbss *fsKeypairMgrSuite) TestOpenWorldWritableFail(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + // make it world-writable + oldUmask := syscall.Umask(0) + os.MkdirAll(filepath.Join(topDir, "private-keys-v1"), 0777) + syscall.Umask(oldUmask) + + bs, err := asserts.OpenFSKeypairManager(topDir) + c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") + c.Check(bs, IsNil) +} diff --git a/asserts/gpgkeypairmgr.go b/asserts/gpgkeypairmgr.go new file mode 100644 index 00000000..90249751 --- /dev/null +++ b/asserts/gpgkeypairmgr.go @@ -0,0 +1,359 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/snapcore/snapd/osutil" +) + +func ensureGPGHomeDirectory() (string, error) { + real, err := osutil.RealUser() + if err != nil { + return "", err + } + + uid, err := strconv.Atoi(real.Uid) + if err != nil { + return "", err + } + + gid, err := strconv.Atoi(real.Gid) + if err != nil { + return "", err + } + + homedir := os.Getenv("SNAP_GNUPG_HOME") + if homedir == "" { + homedir = filepath.Join(real.HomeDir, ".snap", "gnupg") + } + + if err := osutil.MkdirAllChown(homedir, 0700, uid, gid); err != nil { + return "", err + } + return homedir, nil +} + +// findGPGCommand returns the path to a suitable GnuPG binary to use. +// GnuPG 2 is mainly intended for desktop use, and is hard for us to use +// here: in particular, it's extremely difficult to use it to delete a +// secret key without a pinentry prompt (which would be necessary in our +// test suite). GnuPG 1 is still supported so it's reasonable to continue +// using that for now. +func findGPGCommand() (string, error) { + if path := os.Getenv("SNAP_GNUPG_CMD"); path != "" { + return path, nil + } + + path, err := exec.LookPath("gpg1") + if err != nil { + path, err = exec.LookPath("gpg") + } + return path, err +} + +func runGPGImpl(input []byte, args ...string) ([]byte, error) { + homedir, err := ensureGPGHomeDirectory() + if err != nil { + return nil, err + } + + // Ensure the gpg-agent knows what tty to talk to to ask for + // the passphrase. This is needed because we drive gpg over + // a pipe and if the agent is not already started it will + // fail to be able to ask for a password. + if os.Getenv("GPG_TTY") == "" { + tty, err := os.Readlink("/proc/self/fd/0") + if err != nil { + return nil, err + } + os.Setenv("GPG_TTY", tty) + } + + general := []string{"--homedir", homedir, "-q", "--no-auto-check-trustdb"} + allArgs := append(general, args...) + + path, err := findGPGCommand() + if err != nil { + return nil, err + } + cmd := exec.Command(path, allArgs...) + var outBuf bytes.Buffer + var errBuf bytes.Buffer + + if len(input) != 0 { + cmd.Stdin = bytes.NewBuffer(input) + } + + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("%s %s failed: %v (%q)", path, strings.Join(args, " "), err, errBuf.Bytes()) + } + + return outBuf.Bytes(), nil +} + +var runGPG = runGPGImpl + +// A key pair manager backed by a local GnuPG setup. +type GPGKeypairManager struct{} + +func (gkm *GPGKeypairManager) gpg(input []byte, args ...string) ([]byte, error) { + return runGPG(input, args...) +} + +// NewGPGKeypairManager creates a new key pair manager backed by a local GnuPG setup. +// Importing keys through the keypair manager interface is not +// suppored. +// Main purpose is allowing signing using keys from a GPG setup. +func NewGPGKeypairManager() *GPGKeypairManager { + return &GPGKeypairManager{} +} + +func (gkm *GPGKeypairManager) retrieve(fpr string) (PrivateKey, error) { + out, err := gkm.gpg(nil, "--batch", "--export", "--export-options", "export-minimal,export-clean,no-export-attributes", "0x"+fpr) + if err != nil { + return nil, err + } + if len(out) == 0 { + return nil, fmt.Errorf("cannot retrieve key with fingerprint %q in GPG keyring", fpr) + } + + pubKeyBuf := bytes.NewBuffer(out) + privKey, err := newExtPGPPrivateKey(pubKeyBuf, "GPG", func(content []byte) ([]byte, error) { + return gkm.sign(fpr, content) + }) + if err != nil { + return nil, fmt.Errorf("cannot load GPG public key with fingerprint %q: %v", fpr, err) + } + gotFingerprint := privKey.fingerprint() + if gotFingerprint != fpr { + return nil, fmt.Errorf("got wrong public key from GPG, expected fingerprint %q: %s", fpr, gotFingerprint) + } + return privKey, nil +} + +// Walk iterates over all the RSA private keys in the local GPG setup calling the provided callback until this returns an error +func (gkm *GPGKeypairManager) Walk(consider func(privk PrivateKey, fingerprint string, uid string) error) error { + // see GPG source doc/DETAILS + out, err := gkm.gpg(nil, "--batch", "--list-secret-keys", "--fingerprint", "--with-colons", "--fixed-list-mode") + if err != nil { + return err + } + lines := strings.Split(string(out), "\n") + n := len(lines) + if n > 0 && lines[n-1] == "" { + n-- + } + if n == 0 { + return nil + } + lines = lines[:n] + for j := 0; j < n; j++ { + // sec: line + line := lines[j] + if !strings.HasPrefix(line, "sec:") { + continue + } + secFields := strings.Split(line, ":") + if len(secFields) < 5 { + continue + } + if secFields[3] != "1" { // not RSA + continue + } + keyID := secFields[4] + uid := "" + fpr := "" + var privKey PrivateKey + // look for fpr:, uid: lines, order may vary and gpg2.1 + // may springle additional lines in (like gpr:) + Loop: + for k := j + 1; k < n && !strings.HasPrefix(lines[k], "sec:"); k++ { + switch { + case strings.HasPrefix(lines[k], "fpr:"): + fprFields := strings.Split(lines[k], ":") + // extract "Field 10 - User-ID" + // A FPR record stores the fingerprint here. + if len(fprFields) < 10 { + break Loop + } + fpr = fprFields[9] + if !strings.HasSuffix(fpr, keyID) { + break // strange, skip + } + privKey, err = gkm.retrieve(fpr) + if err != nil { + return err + } + case strings.HasPrefix(lines[k], "uid:"): + uidFields := strings.Split(lines[k], ":") + // extract "*** Field 10 - User-ID" + if len(uidFields) < 10 { + break Loop + } + uid = uidFields[9] + } + } + // sanity checking + if privKey == nil || uid == "" { + continue + } + // collected it all + err = consider(privKey, fpr, uid) + if err != nil { + return err + } + } + return nil +} + +func (gkm *GPGKeypairManager) Put(privKey PrivateKey) error { + // NOTE: we don't need this initially at least and this keypair mgr is not for general arbitrary usage + return fmt.Errorf("cannot import private key into GPG keyring") +} + +func (gkm *GPGKeypairManager) Get(keyID string) (PrivateKey, error) { + stop := errors.New("stop marker") + var hit PrivateKey + match := func(privk PrivateKey, fpr string, uid string) error { + if privk.PublicKey().ID() == keyID { + hit = privk + return stop + } + return nil + } + err := gkm.Walk(match) + if err == stop { + return hit, nil + } + if err != nil { + return nil, err + } + return nil, fmt.Errorf("cannot find key %q in GPG keyring", keyID) +} + +func (gkm *GPGKeypairManager) sign(fingerprint string, content []byte) ([]byte, error) { + out, err := gkm.gpg(content, "--personal-digest-preferences", "SHA512", "--default-key", "0x"+fingerprint, "--detach-sign") + if err != nil { + return nil, fmt.Errorf("cannot sign using GPG: %v", err) + } + return out, nil +} + +type gpgKeypairInfo struct { + privKey PrivateKey + fingerprint string +} + +func (gkm *GPGKeypairManager) findByName(name string) (*gpgKeypairInfo, error) { + stop := errors.New("stop marker") + var hit *gpgKeypairInfo + match := func(privk PrivateKey, fpr string, uid string) error { + if uid == name { + hit = &gpgKeypairInfo{ + privKey: privk, + fingerprint: fpr, + } + return stop + } + return nil + } + err := gkm.Walk(match) + if err == stop { + return hit, nil + } + if err != nil { + return nil, err + } + return nil, fmt.Errorf("cannot find key named %q in GPG keyring", name) +} + +// GetByName looks up a private key by name and returns it. +func (gkm *GPGKeypairManager) GetByName(name string) (PrivateKey, error) { + keyInfo, err := gkm.findByName(name) + if err != nil { + return nil, err + } + return keyInfo.privKey, nil +} + +var generateTemplate = ` +Key-Type: RSA +Key-Length: 4096 +Name-Real: %s +Creation-Date: seconds=%d +Preferences: SHA512 +` + +func (gkm *GPGKeypairManager) parametersForGenerate(passphrase string, name string) string { + fixedCreationTime := v1FixedTimestamp.Unix() + generateParams := fmt.Sprintf(generateTemplate, name, fixedCreationTime) + if passphrase != "" { + generateParams += "Passphrase: " + passphrase + "\n" + } + return generateParams +} + +// Generate creates a new key with the given passphrase and name. +func (gkm *GPGKeypairManager) Generate(passphrase string, name string) error { + _, err := gkm.findByName(name) + if err == nil { + return fmt.Errorf("key named %q already exists in GPG keyring", name) + } + generateParams := gkm.parametersForGenerate(passphrase, name) + _, err = gkm.gpg([]byte(generateParams), "--batch", "--gen-key") + if err != nil { + return err + } + return nil +} + +// Export returns the encoded text of the named public key. +func (gkm *GPGKeypairManager) Export(name string) ([]byte, error) { + keyInfo, err := gkm.findByName(name) + if err != nil { + return nil, err + } + return EncodePublicKey(keyInfo.privKey.PublicKey()) +} + +// Delete removes the named key pair from GnuPG's storage. +func (gkm *GPGKeypairManager) Delete(name string) error { + keyInfo, err := gkm.findByName(name) + if err != nil { + return err + } + _, err = gkm.gpg(nil, "--batch", "--delete-secret-and-public-key", "0x"+keyInfo.fingerprint) + if err != nil { + return err + } + return nil +} diff --git a/asserts/gpgkeypairmgr_test.go b/asserts/gpgkeypairmgr_test.go new file mode 100644 index 00000000..30cbe346 --- /dev/null +++ b/asserts/gpgkeypairmgr_test.go @@ -0,0 +1,331 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "fmt" + "os" + "time" + + . "gopkg.in/check.v1" + + "golang.org/x/crypto/openpgp/packet" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/osutil" +) + +type gpgKeypairMgrSuite struct { + homedir string + keypairMgr asserts.KeypairManager +} + +var _ = Suite(&gpgKeypairMgrSuite{}) + +func (gkms *gpgKeypairMgrSuite) SetUpSuite(c *C) { + if !osutil.FileExists("/usr/bin/gpg1") && !osutil.FileExists("/usr/bin/gpg") { + c.Skip("gpg not installed") + } +} + +func (gkms *gpgKeypairMgrSuite) importKey(key string) { + assertstest.GPGImportKey(gkms.homedir, key) +} + +func (gkms *gpgKeypairMgrSuite) SetUpTest(c *C) { + gkms.homedir = c.MkDir() + os.Setenv("SNAP_GNUPG_HOME", gkms.homedir) + gkms.keypairMgr = asserts.NewGPGKeypairManager() + // import test key + gkms.importKey(assertstest.DevKey) +} + +func (gkms *gpgKeypairMgrSuite) TearDownTest(c *C) { + os.Unsetenv("SNAP_GNUPG_HOME") +} + +func (gkms *gpgKeypairMgrSuite) TestGetPublicKeyLooksGood(c *C) { + got, err := gkms.keypairMgr.Get(assertstest.DevKeyID) + c.Assert(err, IsNil) + keyID := got.PublicKey().ID() + c.Check(keyID, Equals, assertstest.DevKeyID) +} + +func (gkms *gpgKeypairMgrSuite) TestGetNotFound(c *C) { + got, err := gkms.keypairMgr.Get("ffffffffffffffff") + c.Check(err, ErrorMatches, `cannot find key "ffffffffffffffff" in GPG keyring`) + c.Check(got, IsNil) +} + +func (gkms *gpgKeypairMgrSuite) TestUseInSigning(c *C) { + store := assertstest.NewStoreStack("trusted", nil) + + devKey, err := gkms.keypairMgr.Get(assertstest.DevKeyID) + c.Assert(err, IsNil) + + devAcct := assertstest.NewAccount(store, "devel1", map[string]interface{}{ + "account-id": "dev1-id", + }, "") + devAccKey := assertstest.NewAccountKey(store, devAcct, nil, devKey.PublicKey(), "") + + signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkms.keypairMgr, + }) + c.Assert(err, IsNil) + + checkDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + }) + c.Assert(err, IsNil) + // add store key + err = checkDB.Add(store.StoreAccountKey("")) + c.Assert(err, IsNil) + // enable devel key + err = checkDB.Add(devAcct) + c.Assert(err, IsNil) + err = checkDB.Add(devAccKey) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "dev1-id", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + snapBuild, err := signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID) + c.Assert(err, IsNil) + + err = checkDB.Check(snapBuild) + c.Check(err, IsNil) +} + +func (gkms *gpgKeypairMgrSuite) TestGetNotUnique(c *C) { + mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) { + if args[1] == "--list-secret-keys" { + return prev(input, args...) + } + c.Assert(args[1], Equals, "--export") + + pk1, err := rsa.GenerateKey(rand.Reader, 512) + c.Assert(err, IsNil) + pk2, err := rsa.GenerateKey(rand.Reader, 512) + c.Assert(err, IsNil) + + buf := new(bytes.Buffer) + err = packet.NewRSAPublicKey(time.Now(), &pk1.PublicKey).Serialize(buf) + c.Assert(err, IsNil) + err = packet.NewRSAPublicKey(time.Now(), &pk2.PublicKey).Serialize(buf) + c.Assert(err, IsNil) + + return buf.Bytes(), nil + } + restore := asserts.MockRunGPG(mockGPG) + defer restore() + + _, err := gkms.keypairMgr.Get(assertstest.DevKeyID) + c.Check(err, ErrorMatches, `cannot load GPG public key with fingerprint "[A-F0-9]+": cannot select exported public key, found many`) +} + +func (gkms *gpgKeypairMgrSuite) TestUseInSigningBrokenSignature(c *C) { + _, rsaPrivKey := assertstest.ReadPrivKey(assertstest.DevKey) + pgpPrivKey := packet.NewRSAPrivateKey(time.Unix(1, 0), rsaPrivKey) + + var breakSig func(sig *packet.Signature, cont []byte) []byte + + mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) { + if args[1] == "--list-secret-keys" || args[1] == "--export" { + return prev(input, args...) + } + n := len(args) + c.Assert(args[n-1], Equals, "--detach-sign") + + sig := new(packet.Signature) + sig.PubKeyAlgo = packet.PubKeyAlgoRSA + sig.Hash = crypto.SHA512 + sig.CreationTime = time.Now() + + // poking to break the signature + cont := breakSig(sig, input) + + h := sig.Hash.New() + h.Write([]byte(cont)) + + err := sig.Sign(h, pgpPrivKey, nil) + c.Assert(err, IsNil) + + buf := new(bytes.Buffer) + sig.Serialize(buf) + return buf.Bytes(), nil + } + restore := asserts.MockRunGPG(mockGPG) + defer restore() + + signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkms.keypairMgr, + }) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "dev1-id", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + + tests := []struct { + breakSig func(*packet.Signature, []byte) []byte + expectedErr string + }{ + {func(sig *packet.Signature, cont []byte) []byte { + sig.Hash = crypto.SHA1 + return cont + }, "cannot sign assertion: bad GPG produced signature: expected SHA512 digest"}, + {func(sig *packet.Signature, cont []byte) []byte { + return cont[:5] + }, "cannot sign assertion: bad GPG produced signature: it does not verify:.*"}, + } + + for _, t := range tests { + breakSig = t.breakSig + + _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID) + c.Check(err, ErrorMatches, t.expectedErr) + } + +} + +func (gkms *gpgKeypairMgrSuite) TestUseInSigningFailure(c *C) { + mockGPG := func(prev asserts.GPGRunner, input []byte, args ...string) ([]byte, error) { + if args[1] == "--list-secret-keys" || args[1] == "--export" { + return prev(input, args...) + } + n := len(args) + c.Assert(args[n-1], Equals, "--detach-sign") + return nil, fmt.Errorf("boom") + } + restore := asserts.MockRunGPG(mockGPG) + defer restore() + + signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkms.keypairMgr, + }) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "dev1-id", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + + _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, assertstest.DevKeyID) + c.Check(err, ErrorMatches, "cannot sign assertion: cannot sign using GPG: boom") +} + +const shortPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQOYBFdGO7MBCADltsXglnDQdfBw0yOVpKZdkuvSnJKKn1H72PapgAr7ucLqNBCA +js0kltDTa2LQP4vljiTyoMzOMnex4kXwRPlF+poZIEBHDLT0i/6sJ6mDukss1HBR +GgNpU3y49WTXc8qxFY4clhbuqgQmy6bUmaVoo3Z4z7cqbsCepWfx5y+vJwMYqlo3 +Nb4q2+hTKS/o3yLiYB7/hkEhMZrFrOPR5SM7Tz5y7cpF6ObY+JZIp/MK+LsLWLji +fEX/pcOtSjFdQqbcnhJJscXRERlFQDbc+gNmZYZ2RqdH5o46OliHkGhVDVTiW25A +SqhGfnodypbZ9QAPSRvhLrN64AqEsvRb3I13ABEBAAEAB/9cQKg8Nz6sQUkkDm9C +iCK1/qyNYwro9+3VXj9FOCJxEJuqMemUr4TMVnMcDQrchkC5GnpVJGXLw3HVcwFS +amjPhUKAp7aYsg40DcrjuXP27oiFQvWuZGuNT5WNtCNg8WQr9POjIFWqWIYdTHk9 +9Ux79vW7s/Oj62GY9OWHPSilxpq1MjDKo9CSMbLeWxW+gbDxaD7cK7H/ONcz8bZ7 +pRfEhNIx3mEbWaZpWRrf+dSUx2OJbPGRkeFFMbCNapqftse173BZCwUKsW7RTp2S +w8Vpo2Ky63Jlpz1DpoMDBz2vSH7pzaqAdnziI2r0IKiidajXFfpXJpJ3ICo/QhWj +x1eRBADrI4I99zHeyy+12QMpkDrOu+ahF6/emdsm1FIy88TqeBmLkeXCXKZIpU3c +USnxzm0nPNbOl7Nvf2VdAyeAftyag7t38Cud5MXldv/iY0e6oTKzxgha37yr6oRv +PZ6VGwbkBvWti1HL4yx1QnkHFS6ailR9WiiHr3HaWAklZAsC0QQA+hgOi0V9fMZZ +Y4/iFVRI9k1NK3pl0mP7pVTzbcjVYspLdIPQxPDsHJW0z48g23KOt0vL3yZvxdBx +cfYGqIonAX19aMD5D4bNLx616pZs78DKGlOz6iXDcaib+n/uCNWxd5R/0m/zugrB +qklpyIC/uxx+SmkJqqq378ytfvBMzccD/3Y6m3PM0ZnrIkr4Q7cKi9ao9rvM+J7o +ziMgfnKWedNDxNa4tIVYYGPiXsjxY/ASUyxVjUPbkyCy3ubZrew0zQ9+kQbO/6vB +WAg9ffT9M92QbSDjuxgUiC5GfvlCoDgJtuLRHd0YLDgUCS5nwb+teEsOpiNWEGXc +Tr+5HZO+g6wxT6W0BiAoeHh4KYkBOAQTAQIAIgUCV0Y7swIbLwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AACgkQEYacUJMr9p/i5wf/XbEiAe1+Y/ZNMO8PYnq1Nktk +CbZEfQo+QH/9gJpt4p78YseWeUp14gsULLks3xRojlKNzYkqBpJcP7Ex+hQ3LEp7 +9IVbept5md4uuZcU0GFF42WAYXExd2cuxPv3lmWHOPuN63a/xpp0M2vYDfpt63qi +Tly5/P4+NgpD6vAh8zwRHuBV/0mno/QX6cUCLVxq2v1aOqC9zq9B5sdYKQKjsQBP +NOXCt1wPaINkqiW/8w2KhUl6mL6vhO0Onqu/F7M/YNXitv6Z2NFdFUVBh58UZW3C +2jrc8JeRQ4Qlr1oeHh2loYOdZfxFPxRjhsRTnNKY8UHWLfbeI6lMqxR5G3DS+g== +=kQRo +-----END PGP PRIVATE KEY BLOCK----- +` + +func (gkms *gpgKeypairMgrSuite) TestUseInSigningKeyTooShort(c *C) { + gkms.importKey(shortPrivKey) + privk, _ := assertstest.ReadPrivKey(shortPrivKey) + + signDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkms.keypairMgr, + }) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": "dev1-id", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + + _, err = signDB.Sign(asserts.SnapBuildType, headers, nil, privk.PublicKey().ID()) + c.Check(err, ErrorMatches, `cannot sign assertion: signing needs at least a 4096 bits key, got 2048`) +} + +func (gkms *gpgKeypairMgrSuite) TestParametersForGenerate(c *C) { + gpgKeypairMgr := gkms.keypairMgr.(*asserts.GPGKeypairManager) + baseParameters := ` +Key-Type: RSA +Key-Length: 4096 +Name-Real: test-key +Creation-Date: seconds=1451606400 +Preferences: SHA512 +` + + tests := []struct { + passphrase string + extraParameters string + }{ + {"", ""}, + {"secret", "Passphrase: secret\n"}, + } + + for _, test := range tests { + parameters := gpgKeypairMgr.ParametersForGenerate(test.passphrase, "test-key") + c.Check(parameters, Equals, baseParameters+test.extraParameters) + } +} diff --git a/asserts/header_checks.go b/asserts/header_checks.go new file mode 100644 index 00000000..5f2d3baf --- /dev/null +++ b/asserts/header_checks.go @@ -0,0 +1,272 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "crypto" + "encoding/base64" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +// common checks used when decoding/assembling assertions + +func checkExistsString(headers map[string]interface{}, name string) (string, error) { + return checkExistsStringWhat(headers, name, "header") +} + +func checkExistsStringWhat(m map[string]interface{}, name, what string) (string, error) { + value, ok := m[name] + if !ok { + return "", fmt.Errorf("%q %s is mandatory", name, what) + } + s, ok := value.(string) + if !ok { + return "", fmt.Errorf("%q %s must be a string", name, what) + } + return s, nil +} + +func checkNotEmptyString(headers map[string]interface{}, name string) (string, error) { + return checkNotEmptyStringWhat(headers, name, "header") +} + +func checkNotEmptyStringWhat(m map[string]interface{}, name, what string) (string, error) { + s, err := checkExistsStringWhat(m, name, what) + if err != nil { + return "", err + } + if len(s) == 0 { + return "", fmt.Errorf("%q %s should not be empty", name, what) + } + return s, nil +} + +func checkOptionalString(headers map[string]interface{}, name string) (string, error) { + value, ok := headers[name] + if !ok { + return "", nil + } + s, ok := value.(string) + if !ok { + return "", fmt.Errorf("%q header must be a string", name) + } + return s, nil +} + +func checkPrimaryKey(headers map[string]interface{}, primKey string) (string, error) { + value, err := checkNotEmptyString(headers, primKey) + if err != nil { + return "", err + } + if strings.Contains(value, "/") { + return "", fmt.Errorf("%q primary key header cannot contain '/'", primKey) + } + return value, nil +} + +func checkAssertType(assertType *AssertionType) error { + if assertType == nil { + return fmt.Errorf("internal error: assertion type cannot be nil") + } + // sanity check against known canonical + sanity := typeRegistry[assertType.Name] + switch sanity { + case assertType: + // fine, matches canonical + return nil + case nil: + return fmt.Errorf("internal error: unknown assertion type: %q", assertType.Name) + default: + return fmt.Errorf("internal error: unpredefined assertion type for name %q used (unexpected address %p)", assertType.Name, assertType) + } +} + +// use 'defl' default if missing +func checkIntWithDefault(headers map[string]interface{}, name string, defl int) (int, error) { + value, ok := headers[name] + if !ok { + return defl, nil + } + s, ok := value.(string) + if !ok { + return -1, fmt.Errorf("%q header is not an integer: %v", name, value) + } + m, err := strconv.Atoi(s) + if err != nil { + return -1, fmt.Errorf("%q header is not an integer: %v", name, s) + } + return m, nil +} + +func checkInt(headers map[string]interface{}, name string) (int, error) { + valueStr, err := checkNotEmptyString(headers, name) + if err != nil { + return -1, err + } + value, err := strconv.Atoi(valueStr) + if err != nil { + return -1, fmt.Errorf("%q header is not an integer: %v", name, valueStr) + } + return value, nil +} + +func checkRFC3339Date(headers map[string]interface{}, name string) (time.Time, error) { + return checkRFC3339DateWhat(headers, name, "header") +} + +func checkRFC3339DateWhat(m map[string]interface{}, name, what string) (time.Time, error) { + dateStr, err := checkNotEmptyStringWhat(m, name, what) + if err != nil { + return time.Time{}, err + } + date, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return time.Time{}, fmt.Errorf("%q %s is not a RFC3339 date: %v", name, what, err) + } + return date, nil +} + +func checkRFC3339DateWithDefault(headers map[string]interface{}, name string, defl time.Time) (time.Time, error) { + return checkRFC3339DateWithDefaultWhat(headers, name, "header", defl) +} + +func checkRFC3339DateWithDefaultWhat(m map[string]interface{}, name, what string, defl time.Time) (time.Time, error) { + value, ok := m[name] + if !ok { + return defl, nil + } + dateStr, ok := value.(string) + if !ok { + return time.Time{}, fmt.Errorf("%q %s must be a string", name, what) + } + date, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return time.Time{}, fmt.Errorf("%q %s is not a RFC3339 date: %v", name, what, err) + } + return date, nil +} + +func checkUint(headers map[string]interface{}, name string, bitSize int) (uint64, error) { + valueStr, err := checkNotEmptyString(headers, name) + if err != nil { + return 0, err + } + + value, err := strconv.ParseUint(valueStr, 10, bitSize) + if err != nil { + return 0, fmt.Errorf("%q header is not an unsigned integer: %v", name, valueStr) + } + return value, nil +} + +func checkDigest(headers map[string]interface{}, name string, h crypto.Hash) ([]byte, error) { + digestStr, err := checkNotEmptyString(headers, name) + if err != nil { + return nil, err + } + b, err := base64.RawURLEncoding.DecodeString(digestStr) + if err != nil { + return nil, fmt.Errorf("%q header cannot be decoded: %v", name, err) + } + if len(b) != h.Size() { + return nil, fmt.Errorf("%q header does not have the expected bit length: %d", name, len(b)*8) + } + + return b, nil +} + +var anyString = regexp.MustCompile("") + +func checkStringListInMap(m map[string]interface{}, name, what string, pattern *regexp.Regexp) ([]string, error) { + value, ok := m[name] + if !ok { + return nil, nil + } + lst, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf("%s must be a list of strings", what) + } + if len(lst) == 0 { + return nil, nil + } + res := make([]string, len(lst)) + for i, v := range lst { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("%s must be a list of strings", what) + } + if !pattern.MatchString(s) { + return nil, fmt.Errorf("%s contains an invalid element: %q", what, s) + } + res[i] = s + } + return res, nil +} + +func checkStringList(headers map[string]interface{}, name string) ([]string, error) { + return checkStringListMatches(headers, name, anyString) +} + +func checkStringListMatches(headers map[string]interface{}, name string, pattern *regexp.Regexp) ([]string, error) { + return checkStringListInMap(headers, name, fmt.Sprintf("%q header", name), pattern) +} + +func checkStringMatches(headers map[string]interface{}, name string, pattern *regexp.Regexp) (string, error) { + return checkStringMatchesWhat(headers, name, "header", pattern) +} + +func checkStringMatchesWhat(headers map[string]interface{}, name, what string, pattern *regexp.Regexp) (string, error) { + s, err := checkNotEmptyStringWhat(headers, name, what) + if err != nil { + return "", err + } + if !pattern.MatchString(s) { + return "", fmt.Errorf("%q %s contains invalid characters: %q", name, what, s) + } + return s, nil +} + +func checkOptionalBool(headers map[string]interface{}, name string) (bool, error) { + value, ok := headers[name] + if !ok { + return false, nil + } + s, ok := value.(string) + if !ok || (s != "true" && s != "false") { + return false, fmt.Errorf("%q header must be 'true' or 'false'", name) + } + return s == "true", nil +} + +func checkMap(headers map[string]interface{}, name string) (map[string]interface{}, error) { + value, ok := headers[name] + if !ok { + return nil, nil + } + m, ok := value.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%q header must be a map", name) + } + return m, nil +} diff --git a/asserts/headers.go b/asserts/headers.go new file mode 100644 index 00000000..b7c42eb1 --- /dev/null +++ b/asserts/headers.go @@ -0,0 +1,318 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "bytes" + "fmt" + "regexp" + "sort" + "strings" + "unicode/utf8" +) + +var ( + nl = []byte("\n") + nlnl = []byte("\n\n") + + // for basic sanity checking of header names + headerNameSanity = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$") +) + +func parseHeaders(head []byte) (map[string]interface{}, error) { + if !utf8.Valid(head) { + return nil, fmt.Errorf("header is not utf8") + } + headers := make(map[string]interface{}) + lines := strings.Split(string(head), "\n") + for i := 0; i < len(lines); { + entry := lines[i] + nameValueSplit := strings.Index(entry, ":") + if nameValueSplit == -1 { + return nil, fmt.Errorf("header entry missing ':' separator: %q", entry) + } + name := entry[:nameValueSplit] + if !headerNameSanity.MatchString(name) { + return nil, fmt.Errorf("invalid header name: %q", name) + } + + consumed := nameValueSplit + 1 + var value interface{} + var err error + value, i, err = parseEntry(consumed, i, lines, 0) + if err != nil { + return nil, err + } + + if _, ok := headers[name]; ok { + return nil, fmt.Errorf("repeated header: %q", name) + } + + headers[name] = value + } + return headers, nil +} + +const ( + commonPrefix = " " + multilinePrefix = " " + listChar = "-" + listPrefix = commonPrefix + listChar +) + +func nestingPrefix(baseIndent int, prefix string) string { + return strings.Repeat(" ", baseIndent) + prefix +} + +func parseEntry(consumedByIntro int, first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) { + entry := lines[first] + i := first + 1 + if consumedByIntro == len(entry) { + // multiline values + basePrefix := nestingPrefix(baseIndent, commonPrefix) + if i < len(lines) && strings.HasPrefix(lines[i], basePrefix) { + rest := lines[i][len(basePrefix):] + if strings.HasPrefix(rest, listChar) { + // list + return parseList(i, lines, baseIndent) + } + if len(rest) > 0 && rest[0] != ' ' { + // map + return parseMap(i, lines, baseIndent) + } + } + + return parseMultilineText(i, lines, baseIndent) + } + + // simple one-line value + if entry[consumedByIntro] != ' ' { + return nil, -1, fmt.Errorf("header entry should have a space or newline (for multiline) before value: %q", entry) + } + + return entry[consumedByIntro+1:], i, nil +} + +func parseMultilineText(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) { + size := 0 + i := first + j := i + prefix := nestingPrefix(baseIndent, multilinePrefix) + for j < len(lines) { + iline := lines[j] + if !strings.HasPrefix(iline, prefix) { + break + } + size += len(iline) - len(prefix) + 1 + j++ + } + if j == i { + var cur string + if i == len(lines) { + cur = "EOF" + } else { + cur = fmt.Sprintf("%q", lines[i]) + } + return nil, -1, fmt.Errorf("expected %d chars nesting prefix after multiline introduction %q: %s", len(prefix), lines[i-1], cur) + } + + valueBuf := bytes.NewBuffer(make([]byte, 0, size-1)) + valueBuf.WriteString(lines[i][len(prefix):]) + i++ + for i < j { + valueBuf.WriteByte('\n') + valueBuf.WriteString(lines[i][len(prefix):]) + i++ + } + + return valueBuf.String(), i, nil +} + +func parseList(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) { + lst := []interface{}(nil) + j := first + prefix := nestingPrefix(baseIndent, listPrefix) + for j < len(lines) { + if !strings.HasPrefix(lines[j], prefix) { + return lst, j, nil + } + var v interface{} + var err error + v, j, err = parseEntry(len(prefix), j, lines, baseIndent+len(listPrefix)-1) + if err != nil { + return nil, -1, err + } + lst = append(lst, v) + } + return lst, j, nil +} + +func parseMap(first int, lines []string, baseIndent int) (value interface{}, firstAfter int, err error) { + m := make(map[string]interface{}) + j := first + prefix := nestingPrefix(baseIndent, commonPrefix) + for j < len(lines) { + if !strings.HasPrefix(lines[j], prefix) { + return m, j, nil + } + + entry := lines[j][len(prefix):] + keyValueSplit := strings.Index(entry, ":") + if keyValueSplit == -1 { + return nil, -1, fmt.Errorf("map entry missing ':' separator: %q", entry) + } + key := entry[:keyValueSplit] + if !headerNameSanity.MatchString(key) { + return nil, -1, fmt.Errorf("invalid map entry key: %q", key) + } + + consumed := keyValueSplit + 1 + var value interface{} + var err error + value, j, err = parseEntry(len(prefix)+consumed, j, lines, len(prefix)) + if err != nil { + return nil, -1, err + } + + if _, ok := m[key]; ok { + return nil, -1, fmt.Errorf("repeated map entry: %q", key) + } + + m[key] = value + } + return m, j, nil +} + +// checkHeader checks that the header values are strings, or nested lists or maps with strings as the only scalars +func checkHeader(v interface{}) error { + switch x := v.(type) { + case string: + return nil + case []interface{}: + for _, elem := range x { + err := checkHeader(elem) + if err != nil { + return err + } + } + return nil + case map[string]interface{}: + for _, elem := range x { + err := checkHeader(elem) + if err != nil { + return err + } + } + return nil + default: + return fmt.Errorf("header values must be strings or nested lists or maps with strings as the only scalars: %v", v) + } +} + +// checkHeaders checks that headers are of expected types +func checkHeaders(headers map[string]interface{}) error { + for name, value := range headers { + err := checkHeader(value) + if err != nil { + return fmt.Errorf("header %q: %v", name, err) + } + } + return nil +} + +// copyHeader helps deep copying header values to defend against external mutations +func copyHeader(v interface{}) interface{} { + switch x := v.(type) { + case string: + return x + case []interface{}: + res := make([]interface{}, len(x)) + for i, elem := range x { + res[i] = copyHeader(elem) + } + return res + case map[string]interface{}: + res := make(map[string]interface{}, len(x)) + for name, value := range x { + if value == nil { + continue // normalize nils out + } + res[name] = copyHeader(value) + } + return res + default: + panic(fmt.Sprintf("internal error: encountered unexpected value type copying headers: %v", v)) + } +} + +// copyHeader helps deep copying headers to defend against external mutations +func copyHeaders(headers map[string]interface{}) map[string]interface{} { + return copyHeader(headers).(map[string]interface{}) +} + +func appendEntry(buf *bytes.Buffer, intro string, v interface{}, baseIndent int) { + switch x := v.(type) { + case nil: + return // omit + case string: + buf.WriteByte('\n') + buf.WriteString(intro) + if strings.IndexRune(x, '\n') != -1 { + // multiline value => quote by 4-space indenting + buf.WriteByte('\n') + pfx := nestingPrefix(baseIndent, multilinePrefix) + buf.WriteString(pfx) + x = strings.Replace(x, "\n", "\n"+pfx, -1) + } else { + buf.WriteByte(' ') + } + buf.WriteString(x) + case []interface{}: + if len(x) == 0 { + return // simply omit + } + buf.WriteByte('\n') + buf.WriteString(intro) + pfx := nestingPrefix(baseIndent, listPrefix) + for _, elem := range x { + appendEntry(buf, pfx, elem, baseIndent+len(listPrefix)-1) + } + case map[string]interface{}: + if len(x) == 0 { + return // simply omit + } + buf.WriteByte('\n') + buf.WriteString(intro) + // emit entries sorted by key + keys := make([]string, len(x)) + i := 0 + for key := range x { + keys[i] = key + i++ + } + sort.Strings(keys) + pfx := nestingPrefix(baseIndent, commonPrefix) + for _, key := range keys { + appendEntry(buf, pfx+key+":", x[key], len(pfx)) + } + default: + panic(fmt.Sprintf("internal error: encountered unexpected value type formatting headers: %v", v)) + } +} diff --git a/asserts/headers_test.go b/asserts/headers_test.go new file mode 100644 index 00000000..4907b9c8 --- /dev/null +++ b/asserts/headers_test.go @@ -0,0 +1,396 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "bytes" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type headersSuite struct{} + +var _ = Suite(&headersSuite{}) + +func (s *headersSuite) TestParseHeadersSimple(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: 1 +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "1", + "bar": "baz", + }) +} + +func (s *headersSuite) TestParseHeadersMultiline(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + abc + +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "abc\n", + "bar": "baz", + }) + + m, err = asserts.ParseHeaders([]byte(`foo: 1 +bar: + baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "1", + "bar": "baz", + }) + + m, err = asserts.ParseHeaders([]byte(`foo: 1 +bar: + baz + `)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "1", + "bar": "baz\n", + }) + + m, err = asserts.ParseHeaders([]byte(`foo: 1 +bar: + baz + + baz2`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": "1", + "bar": "baz\n\nbaz2", + }) +} + +func (s *headersSuite) TestParseHeadersSimpleList(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + - x + - y + - z +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": []interface{}{"x", "y", "z"}, + "bar": "baz", + }) +} + +func (s *headersSuite) TestParseHeadersListNestedMultiline(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + - x + - + y1 + y2 + + - z +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": []interface{}{"x", "y1\ny2\n", "z"}, + "bar": "baz", + }) + + m, err = asserts.ParseHeaders([]byte(`bar: baz +foo: + - + - u1 + - u2 + - + y1 + y2 + `)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": []interface{}{[]interface{}{"u1", "u2"}, "y1\ny2\n"}, + "bar": "baz", + }) +} + +func (s *headersSuite) TestParseHeadersSimpleMap(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + x: X + yy: YY + z5: +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": map[string]interface{}{ + "x": "X", + "yy": "YY", + "z5": "", + }, + "bar": "baz", + }) +} + +func (s *headersSuite) TestParseHeadersMapNestedMultiline(c *C) { + m, err := asserts.ParseHeaders([]byte(`foo: + x: X + yy: + YY1 + YY2 + u: + - u1 + - u2 +bar: baz`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "foo": map[string]interface{}{ + "x": "X", + "yy": "YY1\nYY2", + "u": []interface{}{"u1", "u2"}, + }, + "bar": "baz", + }) + + m, err = asserts.ParseHeaders([]byte(`one: + two: + three: `)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "one": map[string]interface{}{ + "two": map[string]interface{}{ + "three": "", + }, + }, + }) + + m, err = asserts.ParseHeaders([]byte(`one: + two: + three`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "one": map[string]interface{}{ + "two": "three", + }, + }) + + m, err = asserts.ParseHeaders([]byte(`map-within-map: + lev1: + lev2: x`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "map-within-map": map[string]interface{}{ + "lev1": map[string]interface{}{ + "lev2": "x", + }, + }, + }) + + m, err = asserts.ParseHeaders([]byte(`list-of-maps: + - + entry: foo + bar: baz + - + entry: bar`)) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "list-of-maps": []interface{}{ + map[string]interface{}{ + "entry": "foo", + "bar": "baz", + }, + map[string]interface{}{ + "entry": "bar", + }, + }, + }) +} + +func (s *headersSuite) TestParseHeadersMapErrors(c *C) { + _, err := asserts.ParseHeaders([]byte(`foo: + x X +bar: baz`)) + c.Check(err, ErrorMatches, `map entry missing ':' separator: "x X"`) + + _, err = asserts.ParseHeaders([]byte(`foo: + 0x: X +bar: baz`)) + c.Check(err, ErrorMatches, `invalid map entry key: "0x"`) + + _, err = asserts.ParseHeaders([]byte(`foo: + a: a + a: b`)) + c.Check(err, ErrorMatches, `repeated map entry: "a"`) +} + +func (s *headersSuite) TestParseHeadersErrors(c *C) { + _, err := asserts.ParseHeaders([]byte(`foo: 1 +bar:baz`)) + c.Check(err, ErrorMatches, `header entry should have a space or newline \(for multiline\) before value: "bar:baz"`) + + _, err = asserts.ParseHeaders([]byte(`foo: + - x + - y + - z +bar: baz`)) + c.Check(err, ErrorMatches, `expected 4 chars nesting prefix after multiline introduction "foo:": " - x"`) + + _, err = asserts.ParseHeaders([]byte(`foo: + - x + - y + - z +bar:`)) + c.Check(err, ErrorMatches, `expected 4 chars nesting prefix after multiline introduction "bar:": EOF`) +} + +func (s *headersSuite) TestAppendEntrySimple(c *C) { + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", "baz", 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": "baz", + }) +} + +func (s *headersSuite) TestAppendEntryMultiline(c *C) { + multilines := []string{ + "a\n", + "a\nb", + "baz\n baz1\nbaz2", + "baz\n baz1\nbaz2\n", + "baz\n baz1\nbaz2\n\n", + } + + for _, multiline := range multilines { + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", multiline, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": multiline, + }) + } +} + +func (s *headersSuite) TestAppendEntrySimpleList(c *C) { + lst := []interface{}{"x", "y", "z"} + + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", lst, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": lst, + }) +} + +func (s *headersSuite) TestAppendEntryListNested(c *C) { + lst := []interface{}{"x", "a\nb\n", "", []interface{}{"u1", []interface{}{"w1", "w2"}}} + + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", lst, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": lst, + }) +} + +func (s *headersSuite) TestAppendEntrySimpleMap(c *C) { + mp := map[string]interface{}{ + "x": "X", + "yy": "YY", + "z5": "", + } + + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", mp, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": mp, + }) +} + +func (s *headersSuite) TestAppendEntryNestedMap(c *C) { + mp := map[string]interface{}{ + "x": "X", + "u": []interface{}{"u1", "u2"}, + "yy": "YY1\nYY2", + "m": map[string]interface{}{"a": "A", "b": map[string]interface{}{"x": "X", "y": "Y"}}, + } + + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", mp, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": mp, + }) +} + +func (s *headersSuite) TestAppendEntryOmitting(c *C) { + buf := bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", []interface{}{}, 0) + + m, err := asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + }) + + lst := []interface{}{nil, []interface{}{}, "z"} + + buf = bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", lst, 0) + + m, err = asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + "bar": []interface{}{"z"}, + }) + + buf = bytes.NewBufferString("start: .") + + asserts.AppendEntry(buf, "bar:", map[string]interface{}{}, 0) + + m, err = asserts.ParseHeaders(buf.Bytes()) + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]interface{}{ + "start": ".", + }) +} diff --git a/asserts/ifacedecls.go b/asserts/ifacedecls.go new file mode 100644 index 00000000..ce8f77c6 --- /dev/null +++ b/asserts/ifacedecls.go @@ -0,0 +1,974 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" +) + +// AttrMatchContext has contextual helpers for evaluating attribute constraints. +type AttrMatchContext interface { + PlugAttr(arg string) (interface{}, error) + SlotAttr(arg string) (interface{}, error) +} + +const ( + // feature label for $SLOT()/$PLUG()/$MISSING + dollarAttrConstraintsFeature = "dollar-attr-constraints" +) + +type attrMatcher interface { + match(apath string, v interface{}, ctx AttrMatchContext) error + + feature(flabel string) bool +} + +func chain(path, k string) string { + if path == "" { + return k + } + return fmt.Sprintf("%s.%s", path, k) +} + +type compileContext struct { + dotted string + hadMap bool + wasAlt bool +} + +func (cc compileContext) String() string { + return cc.dotted +} + +func (cc compileContext) keyEntry(k string) compileContext { + return compileContext{ + dotted: chain(cc.dotted, k), + hadMap: true, + wasAlt: false, + } +} + +func (cc compileContext) alt(alt int) compileContext { + return compileContext{ + dotted: fmt.Sprintf("%s/alt#%d/", cc.dotted, alt+1), + hadMap: cc.hadMap, + wasAlt: true, + } +} + +// compileAttrMatcher compiles an attrMatcher derived from constraints, +func compileAttrMatcher(cc compileContext, constraints interface{}) (attrMatcher, error) { + switch x := constraints.(type) { + case map[string]interface{}: + return compileMapAttrMatcher(cc, x) + case []interface{}: + if cc.wasAlt { + return nil, fmt.Errorf("cannot nest alternative constraints directly at %q", cc) + } + return compileAltAttrMatcher(cc, x) + case string: + if !cc.hadMap { + return nil, fmt.Errorf("first level of non alternative constraints must be a set of key-value contraints") + } + if strings.HasPrefix(x, "$") { + if x == "$MISSING" { + return missingAttrMatcher{}, nil + } + return compileEvalAttrMatcher(cc, x) + } + return compileRegexpAttrMatcher(cc, x) + default: + return nil, fmt.Errorf("constraint %q must be a key-value map, regexp or a list of alternative constraints: %v", cc, x) + } +} + +type mapAttrMatcher map[string]attrMatcher + +func compileMapAttrMatcher(cc compileContext, m map[string]interface{}) (attrMatcher, error) { + matcher := make(mapAttrMatcher) + for k, constraint := range m { + matcher1, err := compileAttrMatcher(cc.keyEntry(k), constraint) + if err != nil { + return nil, err + } + matcher[k] = matcher1 + } + return matcher, nil +} + +func matchEntry(apath, k string, matcher1 attrMatcher, v interface{}, ctx AttrMatchContext) error { + apath = chain(apath, k) + // every entry matcher expects the attribute to be set except for $MISSING + if _, ok := matcher1.(missingAttrMatcher); !ok && v == nil { + return fmt.Errorf("attribute %q has constraints but is unset", apath) + } + if err := matcher1.match(apath, v, ctx); err != nil { + return err + } + return nil +} + +func matchList(apath string, matcher attrMatcher, l []interface{}, ctx AttrMatchContext) error { + for i, elem := range l { + if err := matcher.match(chain(apath, strconv.Itoa(i)), elem, ctx); err != nil { + return err + } + } + return nil +} + +func (matcher mapAttrMatcher) feature(flabel string) bool { + for _, matcher1 := range matcher { + if matcher1.feature(flabel) { + return true + } + } + return false +} + +func (matcher mapAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + switch x := v.(type) { + case map[string]interface{}: // maps in attributes look like this + for k, matcher1 := range matcher { + if err := matchEntry(apath, k, matcher1, x[k], ctx); err != nil { + return err + } + } + case []interface{}: + return matchList(apath, matcher, x, ctx) + default: + return fmt.Errorf("attribute %q must be a map", apath) + } + return nil +} + +type missingAttrMatcher struct{} + +func (matcher missingAttrMatcher) feature(flabel string) bool { + return flabel == dollarAttrConstraintsFeature +} + +func (matcher missingAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + if v != nil { + return fmt.Errorf("attribute %q is constrained to be missing but is set", apath) + } + return nil +} + +type evalAttrMatcher struct { + // first iteration supports just $(SLOT|PLUG)(arg) + op string + arg string +} + +var ( + validEvalAttrMatcher = regexp.MustCompile(`^\$(SLOT|PLUG)\((.+)\)$`) +) + +func compileEvalAttrMatcher(cc compileContext, s string) (attrMatcher, error) { + ops := validEvalAttrMatcher.FindStringSubmatch(s) + if len(ops) == 0 { + return nil, fmt.Errorf("cannot compile %q constraint %q: not a valid $SLOT()/$PLUG() constraint", cc, s) + } + return evalAttrMatcher{ + op: ops[1], + arg: ops[2], + }, nil +} + +func (matcher evalAttrMatcher) feature(flabel string) bool { + return flabel == dollarAttrConstraintsFeature +} + +func (matcher evalAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + if ctx == nil { + return fmt.Errorf("attribute %q cannot be matched without context", apath) + } + var comp func(string) (interface{}, error) + switch matcher.op { + case "SLOT": + comp = ctx.SlotAttr + case "PLUG": + comp = ctx.PlugAttr + } + v1, err := comp(matcher.arg) + if err != nil { + return fmt.Errorf("attribute %q constraint $%s(%s) cannot be evaluated: %v", apath, matcher.op, matcher.arg, err) + } + if !reflect.DeepEqual(v, v1) { + return fmt.Errorf("attribute %q does not match $%s(%s): %v != %v", apath, matcher.op, matcher.arg, v, v1) + } + return nil +} + +type regexpAttrMatcher struct { + *regexp.Regexp +} + +func compileRegexpAttrMatcher(cc compileContext, s string) (attrMatcher, error) { + rx, err := regexp.Compile("^(" + s + ")$") + if err != nil { + return nil, fmt.Errorf("cannot compile %q constraint %q: %v", cc, s, err) + } + return regexpAttrMatcher{rx}, nil +} + +func (matcher regexpAttrMatcher) feature(flabel string) bool { + return false +} + +func (matcher regexpAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + var s string + switch x := v.(type) { + case string: + s = x + case bool: + s = strconv.FormatBool(x) + case int64: + s = strconv.FormatInt(x, 10) + case []interface{}: + return matchList(apath, matcher, x, ctx) + default: + return fmt.Errorf("attribute %q must be a scalar or list", apath) + } + if !matcher.Regexp.MatchString(s) { + return fmt.Errorf("attribute %q value %q does not match %v", apath, s, matcher.Regexp) + } + return nil + +} + +type altAttrMatcher struct { + alts []attrMatcher +} + +func compileAltAttrMatcher(cc compileContext, l []interface{}) (attrMatcher, error) { + alts := make([]attrMatcher, len(l)) + for i, constraint := range l { + matcher1, err := compileAttrMatcher(cc.alt(i), constraint) + if err != nil { + return nil, err + } + alts[i] = matcher1 + } + return altAttrMatcher{alts}, nil + +} + +func (matcher altAttrMatcher) feature(flabel string) bool { + for _, alt := range matcher.alts { + if alt.feature(flabel) { + return true + } + } + return false +} + +func (matcher altAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + var firstErr error + for _, alt := range matcher.alts { + err := alt.match(apath, v, ctx) + if err == nil { + return nil + } + if firstErr == nil { + firstErr = err + } + } + apathDescr := "" + if apath != "" { + apathDescr = fmt.Sprintf(" for attribute %q", apath) + } + return fmt.Errorf("no alternative%s matches: %v", apathDescr, firstErr) +} + +// AttributeConstraints implements a set of constraints on the attributes of a slot or plug. +type AttributeConstraints struct { + matcher attrMatcher +} + +func (ac *AttributeConstraints) feature(flabel string) bool { + return ac.matcher.feature(flabel) +} + +// compileAttributeConstraints checks and compiles a mapping or list +// from the assertion format into AttributeConstraints. +func compileAttributeConstraints(constraints interface{}) (*AttributeConstraints, error) { + matcher, err := compileAttrMatcher(compileContext{}, constraints) + if err != nil { + return nil, err + } + return &AttributeConstraints{matcher: matcher}, nil +} + +type fixedAttrMatcher struct { + result error +} + +func (matcher fixedAttrMatcher) feature(flabel string) bool { + return false +} + +func (matcher fixedAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + return matcher.result +} + +var ( + AlwaysMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{nil}} + NeverMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{errors.New("not allowed")}} +) + +// Check checks whether attrs don't match the constraints. +func (c *AttributeConstraints) Check(attrs map[string]interface{}, ctx AttrMatchContext) error { + return c.matcher.match("", attrs, ctx) +} + +// OnClassicConstraint specifies a constraint based whether the system is classic and optional specific distros' sets. +type OnClassicConstraint struct { + Classic bool + SystemIDs []string +} + +// rules + +var ( + validSnapType = regexp.MustCompile("^(?:core|kernel|gadget|app)$") + validDistro = regexp.MustCompile("^[-0-9a-z._]+$") + validSnapID = regexp.MustCompile("^[a-z0-9A-Z]{32}$") // snap-ids look like this + validPublisher = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28}|\\$[A-Z][A-Z0-9_]*)$") // account ids look like snap-ids or are nice identifiers, support our own special markers $MARKER + + validIDConstraints = map[string]*regexp.Regexp{ + "slot-snap-type": validSnapType, + "slot-snap-id": validSnapID, + "slot-publisher-id": validPublisher, + "plug-snap-type": validSnapType, + "plug-snap-id": validSnapID, + "plug-publisher-id": validPublisher, + } +) + +func checkMapOrShortcut(context string, v interface{}) (m map[string]interface{}, invert bool, err error) { + switch x := v.(type) { + case map[string]interface{}: + return x, false, nil + case string: + switch x { + case "true": + return nil, false, nil + case "false": + return nil, true, nil + } + } + return nil, false, errors.New("unexpected type") +} + +type constraintsHolder interface { + setAttributeConstraints(field string, cstrs *AttributeConstraints) + setIDConstraints(field string, cstrs []string) + setOnClassicConstraint(onClassic *OnClassicConstraint) +} + +func baseCompileConstraints(context string, cDef constraintsDef, target constraintsHolder, attrConstraints, idConstraints []string) error { + cMap := cDef.cMap + if cMap == nil { + fixed := AlwaysMatchAttributes // "true" + if cDef.invert { // "false" + fixed = NeverMatchAttributes + } + for _, field := range attrConstraints { + target.setAttributeConstraints(field, fixed) + } + return nil + } + defaultUsed := 0 + for _, field := range idConstraints { + lst, err := checkStringListInMap(cMap, field, fmt.Sprintf("%s in %s", field, context), validIDConstraints[field]) + if err != nil { + return err + } + if lst == nil { + defaultUsed++ + } + target.setIDConstraints(field, lst) + } + for _, field := range attrConstraints { + cstrs := AlwaysMatchAttributes + v := cMap[field] + if v != nil { + var err error + cstrs, err = compileAttributeConstraints(cMap[field]) + if err != nil { + return fmt.Errorf("cannot compile %s in %s: %v", field, context, err) + } + } else { + defaultUsed++ + } + target.setAttributeConstraints(field, cstrs) + } + onClassic := cMap["on-classic"] + if onClassic == nil { + defaultUsed++ + } else { + var c *OnClassicConstraint + switch x := onClassic.(type) { + case string: + switch x { + case "true": + c = &OnClassicConstraint{Classic: true} + case "false": + c = &OnClassicConstraint{Classic: false} + } + case []interface{}: + lst, err := checkStringListInMap(cMap, "on-classic", fmt.Sprintf("on-classic in %s", context), validDistro) + if err != nil { + return err + } + c = &OnClassicConstraint{Classic: true, SystemIDs: lst} + } + if c == nil { + return fmt.Errorf("on-classic in %s must be 'true', 'false' or a list of operating system IDs", context) + } + target.setOnClassicConstraint(c) + } + if defaultUsed == len(attributeConstraints)+len(idConstraints)+1 { + return fmt.Errorf("%s must specify at least one of %s, %s, on-classic", context, strings.Join(attrConstraints, ", "), strings.Join(idConstraints, ", ")) + } + return nil +} + +type rule interface { + setConstraints(field string, cstrs []constraintsHolder) +} + +type constraintsDef struct { + cMap map[string]interface{} + invert bool +} + +type subruleCompiler func(context string, def constraintsDef) (constraintsHolder, error) + +func baseCompileRule(context string, rule interface{}, target rule, subrules []string, compilers map[string]subruleCompiler, defaultOutcome, invertedOutcome map[string]interface{}) error { + rMap, invert, err := checkMapOrShortcut(context, rule) + if err != nil { + return fmt.Errorf("%s must be a map or one of the shortcuts 'true' or 'false'", context) + } + if rMap == nil { + rMap = defaultOutcome // "true" + if invert { + rMap = invertedOutcome // "false" + } + } + defaultUsed := 0 + // compile and set subrules + for _, subrule := range subrules { + v := rMap[subrule] + var lst []interface{} + alternatives := false + switch x := v.(type) { + case nil: + v = defaultOutcome[subrule] + defaultUsed++ + case []interface{}: + alternatives = true + lst = x + } + if lst == nil { // v is map or a string, checked below + lst = []interface{}{v} + } + compiler := compilers[subrule] + if compiler == nil { + panic(fmt.Sprintf("no compiler for %s in %s", subrule, context)) + } + alts := make([]constraintsHolder, len(lst)) + for i, alt := range lst { + subctxt := fmt.Sprintf("%s in %s", subrule, context) + if alternatives { + subctxt = fmt.Sprintf("alternative %d of %s", i+1, subctxt) + } + cMap, invert, err := checkMapOrShortcut(subctxt, alt) + if err != nil || (cMap == nil && alternatives) { + efmt := "%s must be a map" + if !alternatives { + efmt = "%s must be a map or one of the shortcuts 'true' or 'false'" + } + return fmt.Errorf(efmt, subctxt) + } + + cstrs, err := compiler(subctxt, constraintsDef{ + cMap: cMap, + invert: invert, + }) + if err != nil { + return err + } + alts[i] = cstrs + } + target.setConstraints(subrule, alts) + } + if defaultUsed == len(subrules) { + return fmt.Errorf("%s must specify at least one of %s", context, strings.Join(subrules, ", ")) + } + return nil +} + +// PlugRule holds the rule of what is allowed, wrt installation and +// connection, for a plug of a specific interface for a snap. +type PlugRule struct { + Interface string + + AllowInstallation []*PlugInstallationConstraints + DenyInstallation []*PlugInstallationConstraints + + AllowConnection []*PlugConnectionConstraints + DenyConnection []*PlugConnectionConstraints + + AllowAutoConnection []*PlugConnectionConstraints + DenyAutoConnection []*PlugConnectionConstraints +} + +func (r *PlugRule) feature(flabel string) bool { + for _, cs := range [][]*PlugInstallationConstraints{r.AllowInstallation, r.DenyInstallation} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + for _, cs := range [][]*PlugConnectionConstraints{r.AllowConnection, r.DenyConnection, r.AllowAutoConnection, r.DenyAutoConnection} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + return false +} + +func castPlugInstallationConstraints(cstrs []constraintsHolder) (res []*PlugInstallationConstraints) { + res = make([]*PlugInstallationConstraints, len(cstrs)) + for i, cstr := range cstrs { + res[i] = cstr.(*PlugInstallationConstraints) + } + return res +} + +func castPlugConnectionConstraints(cstrs []constraintsHolder) (res []*PlugConnectionConstraints) { + res = make([]*PlugConnectionConstraints, len(cstrs)) + for i, cstr := range cstrs { + res[i] = cstr.(*PlugConnectionConstraints) + } + return res +} + +func (r *PlugRule) setConstraints(field string, cstrs []constraintsHolder) { + if len(cstrs) == 0 { + panic(fmt.Sprintf("cannot set PlugRule field %q to empty", field)) + } + switch cstrs[0].(type) { + case *PlugInstallationConstraints: + switch field { + case "allow-installation": + r.AllowInstallation = castPlugInstallationConstraints(cstrs) + return + case "deny-installation": + r.DenyInstallation = castPlugInstallationConstraints(cstrs) + return + } + case *PlugConnectionConstraints: + switch field { + case "allow-connection": + r.AllowConnection = castPlugConnectionConstraints(cstrs) + return + case "deny-connection": + r.DenyConnection = castPlugConnectionConstraints(cstrs) + return + case "allow-auto-connection": + r.AllowAutoConnection = castPlugConnectionConstraints(cstrs) + return + case "deny-auto-connection": + r.DenyAutoConnection = castPlugConnectionConstraints(cstrs) + return + } + } + panic(fmt.Sprintf("cannot set PlugRule field %q with %T elements", field, cstrs[0])) +} + +// PlugInstallationConstraints specifies a set of constraints on an interface plug relevant to the installation of snap. +type PlugInstallationConstraints struct { + PlugSnapTypes []string + + PlugAttributes *AttributeConstraints + + OnClassic *OnClassicConstraint +} + +func (c *PlugInstallationConstraints) feature(flabel string) bool { + return c.PlugAttributes.feature(flabel) +} + +func (c *PlugInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { + switch field { + case "plug-attributes": + c.PlugAttributes = cstrs + default: + panic("unknown PlugInstallationConstraints field " + field) + } +} + +func (c *PlugInstallationConstraints) setIDConstraints(field string, cstrs []string) { + switch field { + case "plug-snap-type": + c.PlugSnapTypes = cstrs + default: + panic("unknown PlugInstallationConstraints field " + field) + } +} + +func (c *PlugInstallationConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { + c.OnClassic = onClassic +} + +func compilePlugInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { + plugInstCstrs := &PlugInstallationConstraints{} + err := baseCompileConstraints(context, cDef, plugInstCstrs, []string{"plug-attributes"}, []string{"plug-snap-type"}) + if err != nil { + return nil, err + } + return plugInstCstrs, nil +} + +// PlugConnectionConstraints specfies a set of constraints on an +// interface plug for a snap relevant to its connection or +// auto-connection. +type PlugConnectionConstraints struct { + SlotSnapTypes []string + SlotSnapIDs []string + SlotPublisherIDs []string + + PlugAttributes *AttributeConstraints + SlotAttributes *AttributeConstraints + + OnClassic *OnClassicConstraint +} + +func (c *PlugConnectionConstraints) feature(flabel string) bool { + return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) +} + +func (c *PlugConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { + switch field { + case "plug-attributes": + c.PlugAttributes = cstrs + case "slot-attributes": + c.SlotAttributes = cstrs + default: + panic("unknown PlugConnectionConstraints field " + field) + } +} + +func (c *PlugConnectionConstraints) setIDConstraints(field string, cstrs []string) { + switch field { + case "slot-snap-type": + c.SlotSnapTypes = cstrs + case "slot-snap-id": + c.SlotSnapIDs = cstrs + case "slot-publisher-id": + c.SlotPublisherIDs = cstrs + default: + panic("unknown PlugConnectionConstraints field " + field) + } +} + +func (c *PlugConnectionConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { + c.OnClassic = onClassic +} + +var ( + attributeConstraints = []string{"plug-attributes", "slot-attributes"} + plugIDConstraints = []string{"slot-snap-type", "slot-publisher-id", "slot-snap-id"} +) + +func compilePlugConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { + plugConnCstrs := &PlugConnectionConstraints{} + err := baseCompileConstraints(context, cDef, plugConnCstrs, attributeConstraints, plugIDConstraints) + if err != nil { + return nil, err + } + return plugConnCstrs, nil +} + +var ( + defaultOutcome = map[string]interface{}{ + "allow-installation": "true", + "allow-connection": "true", + "allow-auto-connection": "true", + "deny-installation": "false", + "deny-connection": "false", + "deny-auto-connection": "false", + } + + invertedOutcome = map[string]interface{}{ + "allow-installation": "false", + "allow-connection": "false", + "allow-auto-connection": "false", + "deny-installation": "true", + "deny-connection": "true", + "deny-auto-connection": "true", + } + + ruleSubrules = []string{"allow-installation", "deny-installation", "allow-connection", "deny-connection", "allow-auto-connection", "deny-auto-connection"} +) + +var plugRuleCompilers = map[string]subruleCompiler{ + "allow-installation": compilePlugInstallationConstraints, + "deny-installation": compilePlugInstallationConstraints, + "allow-connection": compilePlugConnectionConstraints, + "deny-connection": compilePlugConnectionConstraints, + "allow-auto-connection": compilePlugConnectionConstraints, + "deny-auto-connection": compilePlugConnectionConstraints, +} + +func compilePlugRule(interfaceName string, rule interface{}) (*PlugRule, error) { + context := fmt.Sprintf("plug rule for interface %q", interfaceName) + plugRule := &PlugRule{ + Interface: interfaceName, + } + err := baseCompileRule(context, rule, plugRule, ruleSubrules, plugRuleCompilers, defaultOutcome, invertedOutcome) + if err != nil { + return nil, err + } + return plugRule, nil +} + +// SlotRule holds the rule of what is allowed, wrt installation and +// connection, for a slot of a specific interface for a snap. +type SlotRule struct { + Interface string + + AllowInstallation []*SlotInstallationConstraints + DenyInstallation []*SlotInstallationConstraints + + AllowConnection []*SlotConnectionConstraints + DenyConnection []*SlotConnectionConstraints + + AllowAutoConnection []*SlotConnectionConstraints + DenyAutoConnection []*SlotConnectionConstraints +} + +func castSlotInstallationConstraints(cstrs []constraintsHolder) (res []*SlotInstallationConstraints) { + res = make([]*SlotInstallationConstraints, len(cstrs)) + for i, cstr := range cstrs { + res[i] = cstr.(*SlotInstallationConstraints) + } + return res +} + +func (r *SlotRule) feature(flabel string) bool { + for _, cs := range [][]*SlotInstallationConstraints{r.AllowInstallation, r.DenyInstallation} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + for _, cs := range [][]*SlotConnectionConstraints{r.AllowConnection, r.DenyConnection, r.AllowAutoConnection, r.DenyAutoConnection} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + return false +} + +func castSlotConnectionConstraints(cstrs []constraintsHolder) (res []*SlotConnectionConstraints) { + res = make([]*SlotConnectionConstraints, len(cstrs)) + for i, cstr := range cstrs { + res[i] = cstr.(*SlotConnectionConstraints) + } + return res +} + +func (r *SlotRule) setConstraints(field string, cstrs []constraintsHolder) { + if len(cstrs) == 0 { + panic(fmt.Sprintf("cannot set SlotRule field %q to empty", field)) + } + switch cstrs[0].(type) { + case *SlotInstallationConstraints: + switch field { + case "allow-installation": + r.AllowInstallation = castSlotInstallationConstraints(cstrs) + return + case "deny-installation": + r.DenyInstallation = castSlotInstallationConstraints(cstrs) + return + } + case *SlotConnectionConstraints: + switch field { + case "allow-connection": + r.AllowConnection = castSlotConnectionConstraints(cstrs) + return + case "deny-connection": + r.DenyConnection = castSlotConnectionConstraints(cstrs) + return + case "allow-auto-connection": + r.AllowAutoConnection = castSlotConnectionConstraints(cstrs) + return + case "deny-auto-connection": + r.DenyAutoConnection = castSlotConnectionConstraints(cstrs) + return + } + } + panic(fmt.Sprintf("cannot set SlotRule field %q with %T elements", field, cstrs[0])) +} + +// SlotInstallationConstraints specifies a set of constraints on an +// interface slot relevant to the installation of snap. +type SlotInstallationConstraints struct { + SlotSnapTypes []string + + SlotAttributes *AttributeConstraints + + OnClassic *OnClassicConstraint +} + +func (c *SlotInstallationConstraints) feature(flabel string) bool { + return c.SlotAttributes.feature(flabel) +} + +func (c *SlotInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { + switch field { + case "slot-attributes": + c.SlotAttributes = cstrs + default: + panic("unknown SlotInstallationConstraints field " + field) + } +} + +func (c *SlotInstallationConstraints) setIDConstraints(field string, cstrs []string) { + switch field { + case "slot-snap-type": + c.SlotSnapTypes = cstrs + default: + panic("unknown SlotInstallationConstraints field " + field) + } +} + +func (c *SlotInstallationConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { + c.OnClassic = onClassic +} + +func compileSlotInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { + slotInstCstrs := &SlotInstallationConstraints{} + err := baseCompileConstraints(context, cDef, slotInstCstrs, []string{"slot-attributes"}, []string{"slot-snap-type"}) + if err != nil { + return nil, err + } + return slotInstCstrs, nil +} + +// SlotConnectionConstraints specfies a set of constraints on an +// interface slot for a snap relevant to its connection or +// auto-connection. +type SlotConnectionConstraints struct { + PlugSnapTypes []string + PlugSnapIDs []string + PlugPublisherIDs []string + + SlotAttributes *AttributeConstraints + PlugAttributes *AttributeConstraints + + OnClassic *OnClassicConstraint +} + +func (c *SlotConnectionConstraints) feature(flabel string) bool { + return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) +} + +func (c *SlotConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { + switch field { + case "plug-attributes": + c.PlugAttributes = cstrs + case "slot-attributes": + c.SlotAttributes = cstrs + default: + panic("unknown SlotConnectionConstraints field " + field) + } +} + +func (c *SlotConnectionConstraints) setIDConstraints(field string, cstrs []string) { + switch field { + case "plug-snap-type": + c.PlugSnapTypes = cstrs + case "plug-snap-id": + c.PlugSnapIDs = cstrs + case "plug-publisher-id": + c.PlugPublisherIDs = cstrs + default: + panic("unknown SlotConnectionConstraints field " + field) + } +} + +var ( + slotIDConstraints = []string{"plug-snap-type", "plug-publisher-id", "plug-snap-id"} +) + +func (c *SlotConnectionConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { + c.OnClassic = onClassic +} + +func compileSlotConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { + slotConnCstrs := &SlotConnectionConstraints{} + err := baseCompileConstraints(context, cDef, slotConnCstrs, attributeConstraints, slotIDConstraints) + if err != nil { + return nil, err + } + return slotConnCstrs, nil +} + +var slotRuleCompilers = map[string]subruleCompiler{ + "allow-installation": compileSlotInstallationConstraints, + "deny-installation": compileSlotInstallationConstraints, + "allow-connection": compileSlotConnectionConstraints, + "deny-connection": compileSlotConnectionConstraints, + "allow-auto-connection": compileSlotConnectionConstraints, + "deny-auto-connection": compileSlotConnectionConstraints, +} + +func compileSlotRule(interfaceName string, rule interface{}) (*SlotRule, error) { + context := fmt.Sprintf("slot rule for interface %q", interfaceName) + slotRule := &SlotRule{ + Interface: interfaceName, + } + err := baseCompileRule(context, rule, slotRule, ruleSubrules, slotRuleCompilers, defaultOutcome, invertedOutcome) + if err != nil { + return nil, err + } + return slotRule, nil +} diff --git a/asserts/ifacedecls_test.go b/asserts/ifacedecls_test.go new file mode 100644 index 00000000..8b274e89 --- /dev/null +++ b/asserts/ifacedecls_test.go @@ -0,0 +1,1362 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "regexp" + + . "gopkg.in/check.v1" + "gopkg.in/yaml.v2" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/snap" +) + +var ( + _ = Suite(&attrConstraintsSuite{}) + _ = Suite(&plugSlotRulesSuite{}) +) + +type attrConstraintsSuite struct{} + +func attrs(yml string) map[string]interface{} { + var attrs map[string]interface{} + err := yaml.Unmarshal([]byte(yml), &attrs) + if err != nil { + panic(err) + } + snapYaml, err := yaml.Marshal(map[string]interface{}{ + "name": "sample", + "plugs": map[string]interface{}{ + "plug": attrs, + }, + }) + if err != nil { + panic(err) + } + info, err := snap.InfoFromSnapYaml(snapYaml) + if err != nil { + panic(err) + } + return info.Plugs["plug"].Attrs +} + +func (s *attrConstraintsSuite) TestSimple(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: FOO + bar: BAR`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BAR", + "baz": "BAZ", + }, nil) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BAZ", + "baz": "BAZ", + }, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BAZ" does not match \^\(BAR\)\$`) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "baz": "BAZ", + }, nil) + c.Check(err, ErrorMatches, `attribute "bar" has constraints but is unset`) +} + +func (s *attrConstraintsSuite) TestSimpleAnchorsVsRegexpAlt(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + bar: BAR|BAZ`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BAR", + }, nil) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BARR", + }, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BARR" does not match \^\(BAR|BAZ\)\$`) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BBAZ", + }, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BAZZ" does not match \^\(BAR|BAZ\)\$`) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BABAZ", + }, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BABAZ" does not match \^\(BAR|BAZ\)\$`) + + err = cstrs.Check(map[string]interface{}{ + "bar": "BARAZ", + }, nil) + c.Check(err, ErrorMatches, `attribute "bar" value "BARAZ" does not match \^\(BAR|BAZ\)\$`) +} + +func (s *attrConstraintsSuite) TestNested(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: FOO + bar: + bar1: BAR1 + bar2: BAR2`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR2 + bar3: BAR3 +baz: BAZ +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: BAZ +baz: BAZ +`), nil) + c.Check(err, ErrorMatches, `attribute "bar" must be a map`) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR22 + bar3: BAR3 +baz: BAZ +`), nil) + c.Check(err, ErrorMatches, `attribute "bar\.bar2" value "BAR22" does not match \^\(BAR2\)\$`) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: + bar22: true + bar3: BAR3 +baz: BAZ +`), nil) + c.Check(err, ErrorMatches, `attribute "bar\.bar2" must be a scalar or list`) +} + +func (s *attrConstraintsSuite) TestAlternative(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + - + foo: FOO + bar: BAR + - + foo: FOO + bar: BAZ`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].([]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BAR", + "baz": "BAZ", + }, nil) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BAZ", + "baz": "BAZ", + }, nil) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": "FOO", + "bar": "BARR", + "baz": "BAR", + }, nil) + c.Check(err, ErrorMatches, `no alternative matches: attribute "bar" value "BARR" does not match \^\(BAR\)\$`) +} + +func (s *attrConstraintsSuite) TestNestedAlternative(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: FOO + bar: + bar1: BAR1 + bar2: + - BAR2 + - BAR22`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR2 +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR22 +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: FOO +bar: + bar1: BAR1 + bar2: BAR3 +`), nil) + c.Check(err, ErrorMatches, `no alternative for attribute "bar\.bar2" matches: attribute "bar\.bar2" value "BAR3" does not match \^\(BAR2\)\$`) +} + +func (s *attrConstraintsSuite) TestOtherScalars(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: 1 + bar: true`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: 1 +bar: true +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(map[string]interface{}{ + "foo": int64(1), + "bar": true, + }, nil) + c.Check(err, IsNil) +} + +func (s *attrConstraintsSuite) TestCompileErrors(c *C) { + _, err := asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": "[", + }) + c.Check(err, ErrorMatches, `cannot compile "foo" constraint "\[": error parsing regexp:.*`) + + _, err = asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": []interface{}{"foo", "["}, + }) + c.Check(err, ErrorMatches, `cannot compile "foo/alt#2/" constraint "\[": error parsing regexp:.*`) + + _, err = asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": []interface{}{"foo", []interface{}{"bar", "baz"}}, + }) + c.Check(err, ErrorMatches, `cannot nest alternative constraints directly at "foo/alt#2/"`) + + _, err = asserts.CompileAttributeConstraints("FOO") + c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value contraints`) + + _, err = asserts.CompileAttributeConstraints([]interface{}{"FOO"}) + c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value contraints`) + + wrongDollarConstraints := []string{ + "$", + "$FOO(a)", + "$SLOT", + "$SLOT()", + } + + for _, wrong := range wrongDollarConstraints { + _, err := asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": wrong, + }) + c.Check(err, ErrorMatches, fmt.Sprintf(`cannot compile "foo" constraint "%s": not a valid \$SLOT\(\)/\$PLUG\(\) constraint`, regexp.QuoteMeta(wrong))) + + } +} + +func (s *attrConstraintsSuite) TestMatchingListsSimple(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: /foo/.*`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: ["/foo/x", "/foo/y"] +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: ["/foo/x", "/foo"] +`), nil) + c.Check(err, ErrorMatches, `attribute "foo\.1" value "/foo" does not match \^\(/foo/\.\*\)\$`) +} + +func (s *attrConstraintsSuite) TestMissingCheck(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: $MISSING`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(cstrs, "dollar-attr-constraints"), Equals, true) + + err = cstrs.Check(attrs(` +bar: baz +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: ["x"] +`), nil) + c.Check(err, ErrorMatches, `attribute "foo" is constrained to be missing but is set`) +} + +type testEvalAttr struct { + comp func(side string, arg string) (interface{}, error) +} + +func (ca testEvalAttr) SlotAttr(arg string) (interface{}, error) { + return ca.comp("slot", arg) +} + +func (ca testEvalAttr) PlugAttr(arg string) (interface{}, error) { + return ca.comp("plug", arg) +} + +func (s *attrConstraintsSuite) TestEvalCheck(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: $SLOT(foo) + bar: $PLUG(bar.baz)`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(cstrs, "dollar-attr-constraints"), Equals, true) + + err = cstrs.Check(attrs(` +foo: foo +bar: bar +`), nil) + c.Check(err, ErrorMatches, `attribute "(foo|bar)" cannot be matched without context`) + + calls := make(map[[2]string]bool) + comp1 := func(op string, arg string) (interface{}, error) { + calls[[2]string{op, arg}] = true + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp1}) + c.Check(err, IsNil) + + c.Check(calls, DeepEquals, map[[2]string]bool{ + {"slot", "foo"}: true, + {"plug", "bar.baz"}: true, + }) + + comp2 := func(op string, arg string) (interface{}, error) { + if op == "plug" { + return nil, fmt.Errorf("boom") + } + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp2}) + c.Check(err, ErrorMatches, `attribute "bar" constraint \$PLUG\(bar\.baz\) cannot be evaluated: boom`) + + comp3 := func(op string, arg string) (interface{}, error) { + if op == "slot" { + return "other-value", nil + } + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp3}) + c.Check(err, ErrorMatches, `attribute "foo" does not match \$SLOT\(foo\): foo != other-value`) +} + +func (s *attrConstraintsSuite) TestMatchingListsMap(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: + p: /foo/.*`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(attrs(` +foo: [{p: "/foo/x"}, {p: "/foo/y"}] +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: [{p: "zzz"}, {p: "/foo/y"}] +`), nil) + c.Check(err, ErrorMatches, `attribute "foo\.0\.p" value "zzz" does not match \^\(/foo/\.\*\)\$`) +} + +func (s *attrConstraintsSuite) TestAlwaysMatchAttributeConstraints(c *C) { + c.Check(asserts.AlwaysMatchAttributes.Check(nil, nil), IsNil) +} + +func (s *attrConstraintsSuite) TestNeverMatchAttributeConstraints(c *C) { + c.Check(asserts.NeverMatchAttributes.Check(nil, nil), NotNil) +} + +type plugSlotRulesSuite struct{} + +func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected string) { + c.Check(attrs.Check(map[string]interface{}{ + witness: "XYZ", + }, nil), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) + c.Check(attrs.Check(map[string]interface{}{ + witness: expected, + }, nil), IsNil) +} + +func checkBoolPlugConnConstraints(c *C, cstrs []*asserts.PlugConnectionConstraints, always bool) { + expected := asserts.NeverMatchAttributes + if always { + expected = asserts.AlwaysMatchAttributes + } + c.Assert(cstrs, HasLen, 1) + cstrs1 := cstrs[0] + c.Check(cstrs1.PlugAttributes, Equals, expected) + c.Check(cstrs1.SlotAttributes, Equals, expected) + c.Check(cstrs1.SlotSnapIDs, HasLen, 0) + c.Check(cstrs1.SlotPublisherIDs, HasLen, 0) + c.Check(cstrs1.SlotSnapTypes, HasLen, 0) +} + +func checkBoolSlotConnConstraints(c *C, cstrs []*asserts.SlotConnectionConstraints, always bool) { + expected := asserts.NeverMatchAttributes + if always { + expected = asserts.AlwaysMatchAttributes + } + c.Assert(cstrs, HasLen, 1) + cstrs1 := cstrs[0] + c.Check(cstrs1.PlugAttributes, Equals, expected) + c.Check(cstrs1.SlotAttributes, Equals, expected) + c.Check(cstrs1.PlugSnapIDs, HasLen, 0) + c.Check(cstrs1.PlugPublisherIDs, HasLen, 0) + c.Check(cstrs1.PlugSnapTypes, HasLen, 0) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleAllAllowDenyStanzas(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: + plug-attributes: + a1: A1 + deny-installation: + plug-attributes: + a2: A2 + allow-connection: + plug-attributes: + pa3: PA3 + slot-attributes: + sa3: SA3 + deny-connection: + plug-attributes: + pa4: PA4 + slot-attributes: + sa4: SA4 + allow-auto-connection: + plug-attributes: + pa5: PA5 + slot-attributes: + sa5: SA5 + deny-auto-connection: + plug-attributes: + pa6: PA6 + slot-attributes: + sa6: SA6`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + checkAttrs(c, rule.AllowInstallation[0].PlugAttributes, "a1", "A1") + c.Assert(rule.DenyInstallation, HasLen, 1) + checkAttrs(c, rule.DenyInstallation[0].PlugAttributes, "a2", "A2") + // connection subrules + c.Assert(rule.AllowConnection, HasLen, 1) + checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3") + checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3") + c.Assert(rule.DenyConnection, HasLen, 1) + checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4") + checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4") + // auto-connection subrules + c.Assert(rule.AllowAutoConnection, HasLen, 1) + checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5") + checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5") + c.Assert(rule.DenyAutoConnection, HasLen, 1) + checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6") + checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6") +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleAllAllowDenyOrStanzas(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: + - + plug-attributes: + a1: A1 + - + plug-attributes: + a1: A1alt + deny-installation: + - + plug-attributes: + a2: A2 + - + plug-attributes: + a2: A2alt + allow-connection: + - + plug-attributes: + pa3: PA3 + slot-attributes: + sa3: SA3 + - + plug-attributes: + pa3: PA3alt + deny-connection: + - + plug-attributes: + pa4: PA4 + slot-attributes: + sa4: SA4 + - + plug-attributes: + pa4: PA4alt + allow-auto-connection: + - + plug-attributes: + pa5: PA5 + slot-attributes: + sa5: SA5 + - + plug-attributes: + pa5: PA5alt + deny-auto-connection: + - + plug-attributes: + pa6: PA6 + slot-attributes: + sa6: SA6 + - + plug-attributes: + pa6: PA6alt`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 2) + checkAttrs(c, rule.AllowInstallation[0].PlugAttributes, "a1", "A1") + checkAttrs(c, rule.AllowInstallation[1].PlugAttributes, "a1", "A1alt") + c.Assert(rule.DenyInstallation, HasLen, 2) + checkAttrs(c, rule.DenyInstallation[0].PlugAttributes, "a2", "A2") + checkAttrs(c, rule.DenyInstallation[1].PlugAttributes, "a2", "A2alt") + // connection subrules + c.Assert(rule.AllowConnection, HasLen, 2) + checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3") + checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3") + checkAttrs(c, rule.AllowConnection[1].PlugAttributes, "pa3", "PA3alt") + c.Assert(rule.DenyConnection, HasLen, 2) + checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4") + checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4") + checkAttrs(c, rule.DenyConnection[1].PlugAttributes, "pa4", "PA4alt") + // auto-connection subrules + c.Assert(rule.AllowAutoConnection, HasLen, 2) + checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5") + checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5") + checkAttrs(c, rule.AllowAutoConnection[1].PlugAttributes, "pa5", "PA5alt") + c.Assert(rule.DenyAutoConnection, HasLen, 2) + checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6") + checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6") + checkAttrs(c, rule.DenyAutoConnection[1].PlugAttributes, "pa6", "PA6alt") +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleShortcutTrue(c *C) { + rule, err := asserts.CompilePlugRule("iface", "true") + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + // connection subrules + checkBoolPlugConnConstraints(c, rule.AllowConnection, true) + checkBoolPlugConnConstraints(c, rule.DenyConnection, false) + // auto-connection subrules + checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, true) + checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, false) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleShortcutFalse(c *C) { + rule, err := asserts.CompilePlugRule("iface", "false") + c.Assert(err, IsNil) + + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + // connection subrules + checkBoolPlugConnConstraints(c, rule.AllowConnection, false) + checkBoolPlugConnConstraints(c, rule.DenyConnection, true) + // auto-connection subrules + checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, false) + checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, true) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleDefaults(c *C) { + rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ + "deny-auto-connection": "true", + }) + c.Assert(err, IsNil) + + // everything follows the defaults... + + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + // connection subrules + checkBoolPlugConnConstraints(c, rule.AllowConnection, true) + checkBoolPlugConnConstraints(c, rule.DenyConnection, false) + // auto-connection subrules + checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, true) + // ... but deny-auto-connection is on + checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, true) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleInstalationConstraintsIDConstraints(c *C) { + rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ + "allow-installation": map[string]interface{}{ + "plug-snap-type": []interface{}{"core", "kernel", "gadget", "app"}, + }, + }) + c.Assert(err, IsNil) + + c.Assert(rule.AllowInstallation, HasLen, 1) + cstrs := rule.AllowInstallation[0] + c.Check(cstrs.PlugSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"}) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleInstallationConstraintsOnClassic(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, IsNil) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: false`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: true`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: + - ubuntu + - debian`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsIDConstraints(c *C) { + rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ + "allow-connection": map[string]interface{}{ + "slot-snap-type": []interface{}{"core", "kernel", "gadget", "app"}, + "slot-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}, + "slot-publisher-id": []interface{}{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"}, + }, + }) + c.Assert(err, IsNil) + + c.Assert(rule.AllowConnection, HasLen, 1) + cstrs := rule.AllowConnection[0] + c.Check(cstrs.SlotSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"}) + c.Check(cstrs.SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Check(cstrs.SlotPublisherIDs, DeepEquals, []string{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"}) + +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsOnClassic(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, IsNil) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: false`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: true`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: + - ubuntu + - debian`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsAttributesDefault(c *C) { + rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ + "allow-connection": map[string]interface{}{ + "slot-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01"}, + }, + }) + c.Assert(err, IsNil) + + // attributes default to always matching here + cstrs := rule.AllowConnection[0] + c.Check(cstrs.PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Check(cstrs.SlotAttributes, Equals, asserts.AlwaysMatchAttributes) +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleErrors(c *C) { + tests := []struct { + stanza string + err string + }{ + {`iface: foo`, `plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + - allow`, `plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-installation: foo`, `allow-installation in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + deny-installation: foo`, `deny-installation in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-connection: foo`, `allow-connection in plug rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-connection: + - foo`, `alternative 1 of allow-connection in plug rule for interface "iface" must be a map`}, + {`iface: + allow-connection: + - true`, `alternative 1 of allow-connection in plug rule for interface "iface" must be a map`}, + {`iface: + allow-installation: + plug-attributes: + a1: [`, `cannot compile plug-attributes in allow-installation in plug rule for interface "iface": cannot compile "a1" constraint .*`}, + {`iface: + allow-connection: + slot-attributes: + a2: [`, `cannot compile slot-attributes in allow-connection in plug rule for interface "iface": cannot compile "a2" constraint .*`}, + {`iface: + allow-connection: + slot-snap-id: + - + foo: 1`, `slot-snap-id in allow-connection in plug rule for interface "iface" must be a list of strings`}, + {`iface: + allow-connection: + slot-snap-id: + - foo`, `slot-snap-id in allow-connection in plug rule for interface "iface" contains an invalid element: "foo"`}, + {`iface: + allow-connection: + slot-snap-type: + - foo`, `slot-snap-type in allow-connection in plug rule for interface "iface" contains an invalid element: "foo"`}, + {`iface: + allow-connection: + slot-snap-type: + - xapp`, `slot-snap-type in allow-connection in plug rule for interface "iface" contains an invalid element: "xapp"`}, + {`iface: + allow-connection: + slot-snap-ids: + - foo`, `allow-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + {`iface: + deny-connection: + slot-snap-ids: + - foo`, `deny-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + {`iface: + allow-auto-connection: + slot-snap-ids: + - foo`, `allow-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + {`iface: + deny-auto-connection: + slot-snap-ids: + - foo`, `deny-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic`}, + {`iface: + allow-connect: true`, `plug rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`}, + } + + for _, t := range tests { + m, err := asserts.ParseHeaders([]byte(t.stanza)) + c.Assert(err, IsNil, Commentf(t.stanza)) + + _, err = asserts.CompilePlugRule("iface", m["iface"]) + c.Check(err, ErrorMatches, t.err, Commentf(t.stanza)) + } +} + +func (s *plugSlotRulesSuite) TestPlugRuleFeatures(c *C) { + combos := []struct { + subrule string + attrConstraints []string + }{ + {"allow-installation", []string{"plug-attributes"}}, + {"deny-installation", []string{"plug-attributes"}}, + {"allow-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-connection", []string{"plug-attributes", "slot-attributes"}}, + {"allow-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + } + + for _, combo := range combos { + for _, attrConstr := range combo.attrConstraints { + attrConstraintMap := map[string]interface{}{ + "a": "ATTR", + "other": []interface{}{"x", "y"}, + } + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + attrConstr: attrConstraintMap, + }, + } + + rule, err := asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) + + attrConstraintMap["a"] = "$MISSING" + rule, err = asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + // covers also alternation + attrConstraintMap["a"] = []interface{}{"$SLOT(a)"} + rule, err = asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + } + } +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleAllAllowDenyStanzas(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: + slot-attributes: + a1: A1 + deny-installation: + slot-attributes: + a2: A2 + allow-connection: + plug-attributes: + pa3: PA3 + slot-attributes: + sa3: SA3 + deny-connection: + plug-attributes: + pa4: PA4 + slot-attributes: + sa4: SA4 + allow-auto-connection: + plug-attributes: + pa5: PA5 + slot-attributes: + sa5: SA5 + deny-auto-connection: + plug-attributes: + pa6: PA6 + slot-attributes: + sa6: SA6`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + checkAttrs(c, rule.AllowInstallation[0].SlotAttributes, "a1", "A1") + c.Assert(rule.DenyInstallation, HasLen, 1) + checkAttrs(c, rule.DenyInstallation[0].SlotAttributes, "a2", "A2") + // connection subrules + c.Assert(rule.AllowConnection, HasLen, 1) + checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3") + checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3") + c.Assert(rule.DenyConnection, HasLen, 1) + checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4") + checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4") + // auto-connection subrules + c.Assert(rule.AllowAutoConnection, HasLen, 1) + checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5") + checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5") + c.Assert(rule.DenyAutoConnection, HasLen, 1) + checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6") + checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6") +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleAllAllowDenyOrStanzas(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: + - + slot-attributes: + a1: A1 + - + slot-attributes: + a1: A1alt + deny-installation: + - + slot-attributes: + a2: A2 + - + slot-attributes: + a2: A2alt + allow-connection: + - + plug-attributes: + pa3: PA3 + slot-attributes: + sa3: SA3 + - + slot-attributes: + sa3: SA3alt + deny-connection: + - + plug-attributes: + pa4: PA4 + slot-attributes: + sa4: SA4 + - + slot-attributes: + sa4: SA4alt + allow-auto-connection: + - + plug-attributes: + pa5: PA5 + slot-attributes: + sa5: SA5 + - + slot-attributes: + sa5: SA5alt + deny-auto-connection: + - + plug-attributes: + pa6: PA6 + slot-attributes: + sa6: SA6 + - + slot-attributes: + sa6: SA6alt`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 2) + checkAttrs(c, rule.AllowInstallation[0].SlotAttributes, "a1", "A1") + checkAttrs(c, rule.AllowInstallation[1].SlotAttributes, "a1", "A1alt") + c.Assert(rule.DenyInstallation, HasLen, 2) + checkAttrs(c, rule.DenyInstallation[0].SlotAttributes, "a2", "A2") + checkAttrs(c, rule.DenyInstallation[1].SlotAttributes, "a2", "A2alt") + // connection subrules + c.Assert(rule.AllowConnection, HasLen, 2) + checkAttrs(c, rule.AllowConnection[0].PlugAttributes, "pa3", "PA3") + checkAttrs(c, rule.AllowConnection[0].SlotAttributes, "sa3", "SA3") + checkAttrs(c, rule.AllowConnection[1].SlotAttributes, "sa3", "SA3alt") + c.Assert(rule.DenyConnection, HasLen, 2) + checkAttrs(c, rule.DenyConnection[0].PlugAttributes, "pa4", "PA4") + checkAttrs(c, rule.DenyConnection[0].SlotAttributes, "sa4", "SA4") + checkAttrs(c, rule.DenyConnection[1].SlotAttributes, "sa4", "SA4alt") + // auto-connection subrules + c.Assert(rule.AllowAutoConnection, HasLen, 2) + checkAttrs(c, rule.AllowAutoConnection[0].PlugAttributes, "pa5", "PA5") + checkAttrs(c, rule.AllowAutoConnection[0].SlotAttributes, "sa5", "SA5") + checkAttrs(c, rule.AllowAutoConnection[1].SlotAttributes, "sa5", "SA5alt") + c.Assert(rule.DenyAutoConnection, HasLen, 2) + checkAttrs(c, rule.DenyAutoConnection[0].PlugAttributes, "pa6", "PA6") + checkAttrs(c, rule.DenyAutoConnection[0].SlotAttributes, "sa6", "SA6") + checkAttrs(c, rule.DenyAutoConnection[1].SlotAttributes, "sa6", "SA6alt") +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleShortcutTrue(c *C) { + rule, err := asserts.CompileSlotRule("iface", "true") + c.Assert(err, IsNil) + + c.Check(rule.Interface, Equals, "iface") + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + // connection subrules + checkBoolSlotConnConstraints(c, rule.AllowConnection, true) + checkBoolSlotConnConstraints(c, rule.DenyConnection, false) + // auto-connection subrules + checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, true) + checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, false) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleShortcutFalse(c *C) { + rule, err := asserts.CompileSlotRule("iface", "false") + c.Assert(err, IsNil) + + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + // connection subrules + checkBoolSlotConnConstraints(c, rule.AllowConnection, false) + checkBoolSlotConnConstraints(c, rule.DenyConnection, true) + // auto-connection subrules + checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, false) + checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, true) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleDefaults(c *C) { + rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{ + "deny-auto-connection": "true", + }) + c.Assert(err, IsNil) + + // everything follows the defaults... + + // install subrules + c.Assert(rule.AllowInstallation, HasLen, 1) + c.Check(rule.AllowInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(rule.DenyInstallation, HasLen, 1) + c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + // connection subrules + checkBoolSlotConnConstraints(c, rule.AllowConnection, true) + checkBoolSlotConnConstraints(c, rule.DenyConnection, false) + // auto-connection subrules + checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, true) + // ... but deny-auto-connection is on + checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, true) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsIDConstraints(c *C) { + rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{ + "allow-installation": map[string]interface{}{ + "slot-snap-type": []interface{}{"core", "kernel", "gadget", "app"}, + }, + }) + c.Assert(err, IsNil) + + c.Assert(rule.AllowInstallation, HasLen, 1) + cstrs := rule.AllowInstallation[0] + c.Check(cstrs.SlotSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"}) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsOnClassic(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, IsNil) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: false`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: true`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-installation: + on-classic: + - ubuntu + - debian`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsIDConstraints(c *C) { + rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{ + "allow-connection": map[string]interface{}{ + "plug-snap-type": []interface{}{"core", "kernel", "gadget", "app"}, + "plug-snap-id": []interface{}{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}, + "plug-publisher-id": []interface{}{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"}, + }, + }) + c.Assert(err, IsNil) + + c.Assert(rule.AllowConnection, HasLen, 1) + cstrs := rule.AllowConnection[0] + c.Check(cstrs.PlugSnapTypes, DeepEquals, []string{"core", "kernel", "gadget", "app"}) + c.Check(cstrs.PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Check(cstrs.PlugPublisherIDs, DeepEquals, []string{"pubidpubidpubidpubidpubidpubid09", "canonical", "$SAME"}) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsOnClassic(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, IsNil) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: false`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: true`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true}) + + m, err = asserts.ParseHeaders([]byte(`iface: + allow-connection: + on-classic: + - ubuntu + - debian`)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].OnClassic, DeepEquals, &asserts.OnClassicConstraint{Classic: true, SystemIDs: []string{"ubuntu", "debian"}}) +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleErrors(c *C) { + tests := []struct { + stanza string + err string + }{ + {`iface: foo`, `slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + - allow`, `slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-installation: foo`, `allow-installation in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + deny-installation: foo`, `deny-installation in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-connection: foo`, `allow-connection in slot rule for interface "iface" must be a map or one of the shortcuts 'true' or 'false'`}, + {`iface: + allow-connection: + - foo`, `alternative 1 of allow-connection in slot rule for interface "iface" must be a map`}, + {`iface: + allow-connection: + - true`, `alternative 1 of allow-connection in slot rule for interface "iface" must be a map`}, + {`iface: + allow-installation: + slot-attributes: + a1: [`, `cannot compile slot-attributes in allow-installation in slot rule for interface "iface": cannot compile "a1" constraint .*`}, + {`iface: + allow-connection: + plug-attributes: + a2: [`, `cannot compile plug-attributes in allow-connection in slot rule for interface "iface": cannot compile "a2" constraint .*`}, + {`iface: + allow-connection: + plug-snap-id: + - + foo: 1`, `plug-snap-id in allow-connection in slot rule for interface "iface" must be a list of strings`}, + {`iface: + allow-connection: + plug-snap-id: + - foo`, `plug-snap-id in allow-connection in slot rule for interface "iface" contains an invalid element: "foo"`}, + {`iface: + allow-connection: + plug-snap-type: + - foo`, `plug-snap-type in allow-connection in slot rule for interface "iface" contains an invalid element: "foo"`}, + {`iface: + allow-connection: + plug-snap-type: + - xapp`, `plug-snap-type in allow-connection in slot rule for interface "iface" contains an invalid element: "xapp"`}, + {`iface: + allow-connection: + on-classic: + x: 1`, `on-classic in allow-connection in slot rule for interface \"iface\" must be 'true', 'false' or a list of operating system IDs`}, + {`iface: + allow-connection: + on-classic: + - zoom!`, `on-classic in allow-connection in slot rule for interface \"iface\" contains an invalid element: \"zoom!\"`}, + {`iface: + allow-connection: + plug-snap-ids: + - foo`, `allow-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + {`iface: + deny-connection: + plug-snap-ids: + - foo`, `deny-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + {`iface: + allow-auto-connection: + plug-snap-ids: + - foo`, `allow-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + {`iface: + deny-auto-connection: + plug-snap-ids: + - foo`, `deny-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic`}, + {`iface: + allow-connect: true`, `slot rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`}, + } + + for _, t := range tests { + m, err := asserts.ParseHeaders([]byte(t.stanza)) + c.Assert(err, IsNil, Commentf(t.stanza)) + _, err = asserts.CompileSlotRule("iface", m["iface"]) + c.Check(err, ErrorMatches, t.err, Commentf(t.stanza)) + } +} + +func (s *plugSlotRulesSuite) TestSlotRuleFeatures(c *C) { + combos := []struct { + subrule string + attrConstraints []string + }{ + {"allow-installation", []string{"slot-attributes"}}, + {"deny-installation", []string{"slot-attributes"}}, + {"allow-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-connection", []string{"plug-attributes", "slot-attributes"}}, + {"allow-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + } + + for _, combo := range combos { + for _, attrConstr := range combo.attrConstraints { + attrConstraintMap := map[string]interface{}{ + "a": "ATTR", + } + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + attrConstr: attrConstraintMap, + }, + } + + rule, err := asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) + + attrConstraintMap["a"] = "$PLUG(a)" + rule, err = asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + } + } +} diff --git a/asserts/membackstore.go b/asserts/membackstore.go new file mode 100644 index 00000000..a705f1f5 --- /dev/null +++ b/asserts/membackstore.go @@ -0,0 +1,191 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "errors" + "sync" +) + +type memoryBackstore struct { + top memBSBranch + mu sync.RWMutex +} + +type memBSNode interface { + put(assertType *AssertionType, key []string, assert Assertion) error + get(key []string, maxFormat int) (Assertion, error) + search(hint []string, found func(Assertion), maxFormat int) +} + +type memBSBranch map[string]memBSNode + +type memBSLeaf map[string]map[int]Assertion + +func (br memBSBranch) put(assertType *AssertionType, key []string, assert Assertion) error { + key0 := key[0] + down := br[key0] + if down == nil { + if len(key) > 2 { + down = make(memBSBranch) + } else { + down = make(memBSLeaf) + } + br[key0] = down + } + return down.put(assertType, key[1:], assert) +} + +func (leaf memBSLeaf) cur(key0 string, maxFormat int) (a Assertion) { + for formatnum, a1 := range leaf[key0] { + if formatnum <= maxFormat { + if a == nil || a1.Revision() > a.Revision() { + a = a1 + } + } + } + return a +} + +func (leaf memBSLeaf) put(assertType *AssertionType, key []string, assert Assertion) error { + key0 := key[0] + cur := leaf.cur(key0, assertType.MaxSupportedFormat()) + if cur != nil { + rev := assert.Revision() + curRev := cur.Revision() + if curRev >= rev { + return &RevisionError{Current: curRev, Used: rev} + } + } + if _, ok := leaf[key0]; !ok { + leaf[key0] = make(map[int]Assertion) + } + leaf[key0][assert.Format()] = assert + return nil +} + +// errNotFound is used internally by backends, it is converted to the richer +// NotFoundError only at their public interface boundary +var errNotFound = errors.New("assertion not found") + +func (br memBSBranch) get(key []string, maxFormat int) (Assertion, error) { + key0 := key[0] + down := br[key0] + if down == nil { + return nil, errNotFound + } + return down.get(key[1:], maxFormat) +} + +func (leaf memBSLeaf) get(key []string, maxFormat int) (Assertion, error) { + key0 := key[0] + cur := leaf.cur(key0, maxFormat) + if cur == nil { + return nil, errNotFound + } + return cur, nil +} + +func (br memBSBranch) search(hint []string, found func(Assertion), maxFormat int) { + hint0 := hint[0] + if hint0 == "" { + for _, down := range br { + down.search(hint[1:], found, maxFormat) + } + return + } + down := br[hint0] + if down != nil { + down.search(hint[1:], found, maxFormat) + } + return +} + +func (leaf memBSLeaf) search(hint []string, found func(Assertion), maxFormat int) { + hint0 := hint[0] + if hint0 == "" { + for key := range leaf { + cand := leaf.cur(key, maxFormat) + if cand != nil { + found(cand) + } + } + return + } + + cur := leaf.cur(hint0, maxFormat) + if cur != nil { + found(cur) + } +} + +// NewMemoryBackstore creates a memory backed assertions backstore. +func NewMemoryBackstore() Backstore { + return &memoryBackstore{ + top: make(memBSBranch), + } +} + +func (mbs *memoryBackstore) Put(assertType *AssertionType, assert Assertion) error { + mbs.mu.Lock() + defer mbs.mu.Unlock() + + internalKey := make([]string, 1, 1+len(assertType.PrimaryKey)) + internalKey[0] = assertType.Name + internalKey = append(internalKey, assert.Ref().PrimaryKey...) + + err := mbs.top.put(assertType, internalKey, assert) + return err +} + +func (mbs *memoryBackstore) Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) { + mbs.mu.RLock() + defer mbs.mu.RUnlock() + + internalKey := make([]string, 1+len(assertType.PrimaryKey)) + internalKey[0] = assertType.Name + copy(internalKey[1:], key) + + a, err := mbs.top.get(internalKey, maxFormat) + if err == errNotFound { + return nil, &NotFoundError{Type: assertType} + } + return a, err +} + +func (mbs *memoryBackstore) Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error { + mbs.mu.RLock() + defer mbs.mu.RUnlock() + + hint := make([]string, 1+len(assertType.PrimaryKey)) + hint[0] = assertType.Name + for i, name := range assertType.PrimaryKey { + hint[1+i] = headers[name] + } + + candCb := func(a Assertion) { + if searchMatch(a, headers) { + foundCb(a) + } + } + + mbs.top.search(hint, candCb, maxFormat) + return nil +} diff --git a/asserts/membackstore_test.go b/asserts/membackstore_test.go new file mode 100644 index 00000000..644c0604 --- /dev/null +++ b/asserts/membackstore_test.go @@ -0,0 +1,351 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type memBackstoreSuite struct { + bs asserts.Backstore + a asserts.Assertion +} + +var _ = Suite(&memBackstoreSuite{}) + +func (mbss *memBackstoreSuite) SetUpTest(c *C) { + mbss.bs = asserts.NewMemoryBackstore() + + encoded := "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + mbss.a = a +} + +func (mbss *memBackstoreSuite) TestPutAndGet(c *C) { + err := mbss.bs.Put(asserts.TestOnlyType, mbss.a) + c.Assert(err, IsNil) + + a, err := mbss.bs.Get(asserts.TestOnlyType, []string{"foo"}, 0) + c.Assert(err, IsNil) + + c.Check(a, Equals, mbss.a) +} + +func (mbss *memBackstoreSuite) TestGetNotFound(c *C) { + a, err := mbss.bs.Get(asserts.TestOnlyType, []string{"foo"}, 0) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + // Headers can be omitted by Backstores + }) + c.Check(a, IsNil) + + err = mbss.bs.Put(asserts.TestOnlyType, mbss.a) + c.Assert(err, IsNil) + + a, err = mbss.bs.Get(asserts.TestOnlyType, []string{"bar"}, 0) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) + c.Check(a, IsNil) +} + +func (mbss *memBackstoreSuite) TestPutNotNewer(c *C) { + err := mbss.bs.Put(asserts.TestOnlyType, mbss.a) + c.Assert(err, IsNil) + + err = mbss.bs.Put(asserts.TestOnlyType, mbss.a) + c.Check(err, ErrorMatches, "revision 0 is already the current revision") +} + +func (mbss *memBackstoreSuite) TestSearch(c *C) { + encoded := "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: one\n" + + "other: other1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a1, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + encoded = "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: two\n" + + "other: other2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a2, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + err = mbss.bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + err = mbss.bs.Put(asserts.TestOnlyType, a2) + c.Assert(err, IsNil) + + found := map[string]asserts.Assertion{} + cb := func(a asserts.Assertion) { + found[a.HeaderString("primary-key")] = a + } + err = mbss.bs.Search(asserts.TestOnlyType, nil, cb, 0) + c.Assert(err, IsNil) + c.Check(found, HasLen, 2) + + found = map[string]asserts.Assertion{} + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "one", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]asserts.Assertion{ + "one": a1, + }) + + found = map[string]asserts.Assertion{} + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "other": "other2", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]asserts.Assertion{ + "two": a2, + }) + + found = map[string]asserts.Assertion{} + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "two", + "other": "other1", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, HasLen, 0) +} + +func (mbss *memBackstoreSuite) TestSearch2Levels(c *C) { + encoded := "type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: a\n" + + "pk2: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + aAX, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + encoded = "type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: b\n" + + "pk2: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + aBX, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + err = mbss.bs.Put(asserts.TestOnly2Type, aAX) + c.Assert(err, IsNil) + err = mbss.bs.Put(asserts.TestOnly2Type, aBX) + c.Assert(err, IsNil) + + found := map[string]asserts.Assertion{} + cb := func(a asserts.Assertion) { + found[a.HeaderString("pk1")+":"+a.HeaderString("pk2")] = a + } + err = mbss.bs.Search(asserts.TestOnly2Type, map[string]string{ + "pk2": "x", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, HasLen, 2) +} + +func (mbss *memBackstoreSuite) TestPutOldRevision(c *C) { + bs := asserts.NewMemoryBackstore() + + // Create two revisions of assertion. + a0, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + a1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + // Put newer revision, follwed by old revision. + err = bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a0) + + c.Check(err, ErrorMatches, `revision 0 is older than current revision 1`) + c.Check(err, DeepEquals, &asserts.RevisionError{Current: 1, Used: 0}) +} + +func (mbss *memBackstoreSuite) TestGetFormat(c *C) { + bs := asserts.NewMemoryBackstore() + + af0, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: foo\n" + + "format: 1\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af2, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: zoo\n" + + "format: 2\n" + + "revision: 22\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + err = bs.Put(asserts.TestOnlyType, af0) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, af1) + c.Assert(err, IsNil) + + a, err := bs.Get(asserts.TestOnlyType, []string{"foo"}, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + a, err = bs.Get(asserts.TestOnlyType, []string{"foo"}, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 0) + c.Assert(err, FitsTypeOf, &asserts.NotFoundError{}) + + err = bs.Put(asserts.TestOnlyType, af2) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1) + c.Assert(err, FitsTypeOf, &asserts.NotFoundError{}) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 2) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 22) +} + +func (mbss *memBackstoreSuite) TestSearchFormat(c *C) { + bs := asserts.NewMemoryBackstore() + + af0, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: bar\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + af1, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: bar\n" + + "format: 1\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + af2, err := asserts.Decode([]byte("type: test-only-2\n" + + "authority-id: auth-id1\n" + + "pk1: foo\n" + + "pk2: baz\n" + + "format: 2\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + err = bs.Put(asserts.TestOnly2Type, af0) + c.Assert(err, IsNil) + + queries := []map[string]string{ + {"pk1": "foo", "pk2": "bar"}, + {"pk1": "foo"}, + {"pk2": "bar"}, + } + + for _, q := range queries { + var a asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + a = a1 + } + err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + } + + err = bs.Put(asserts.TestOnly2Type, af1) + c.Assert(err, IsNil) + + for _, q := range queries { + var a asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + a = a1 + } + err := bs.Search(asserts.TestOnly2Type, q, foundCb, 1) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + + err = bs.Search(asserts.TestOnly2Type, q, foundCb, 0) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 0) + } + + err = bs.Put(asserts.TestOnly2Type, af2) + c.Assert(err, IsNil) + + var as []asserts.Assertion + foundCb := func(a1 asserts.Assertion) { + as = append(as, a1) + } + err = bs.Search(asserts.TestOnly2Type, map[string]string{ + "pk1": "foo", + }, foundCb, 1) // will not find af2 + c.Assert(err, IsNil) + c.Check(as, HasLen, 1) + c.Check(as[0].Revision(), Equals, 1) + +} diff --git a/asserts/memkeypairmgr.go b/asserts/memkeypairmgr.go new file mode 100644 index 00000000..68293a25 --- /dev/null +++ b/asserts/memkeypairmgr.go @@ -0,0 +1,59 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "sync" +) + +type memoryKeypairManager struct { + pairs map[string]PrivateKey + mu sync.RWMutex +} + +// NewMemoryKeypairManager creates a new key pair manager with a memory backstore. +func NewMemoryKeypairManager() KeypairManager { + return &memoryKeypairManager{ + pairs: make(map[string]PrivateKey), + } +} + +func (mkm *memoryKeypairManager) Put(privKey PrivateKey) error { + mkm.mu.Lock() + defer mkm.mu.Unlock() + + keyID := privKey.PublicKey().ID() + if mkm.pairs[keyID] != nil { + return errKeypairAlreadyExists + } + mkm.pairs[keyID] = privKey + return nil +} + +func (mkm *memoryKeypairManager) Get(keyID string) (PrivateKey, error) { + mkm.mu.RLock() + defer mkm.mu.RUnlock() + + privKey := mkm.pairs[keyID] + if privKey == nil { + return nil, errKeypairNotFound + } + return privKey, nil +} diff --git a/asserts/memkeypairmgr_test.go b/asserts/memkeypairmgr_test.go new file mode 100644 index 00000000..a99018ff --- /dev/null +++ b/asserts/memkeypairmgr_test.go @@ -0,0 +1,73 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type memKeypairMgtSuite struct { + keypairMgr asserts.KeypairManager +} + +var _ = Suite(&memKeypairMgtSuite{}) + +func (mkms *memKeypairMgtSuite) SetUpTest(c *C) { + mkms.keypairMgr = asserts.NewMemoryKeypairManager() +} + +func (mkms *memKeypairMgtSuite) TestPutAndGet(c *C) { + pk1 := testPrivKey1 + keyID := pk1.PublicKey().ID() + err := mkms.keypairMgr.Put(pk1) + c.Assert(err, IsNil) + + got, err := mkms.keypairMgr.Get(keyID) + c.Assert(err, IsNil) + c.Assert(got, NotNil) + c.Check(got.PublicKey().ID(), Equals, pk1.PublicKey().ID()) +} + +func (mkms *memKeypairMgtSuite) TestPutAlreadyExists(c *C) { + pk1 := testPrivKey1 + err := mkms.keypairMgr.Put(pk1) + c.Assert(err, IsNil) + + err = mkms.keypairMgr.Put(pk1) + c.Check(err, ErrorMatches, "key pair with given key id already exists") +} + +func (mkms *memKeypairMgtSuite) TestGetNotFound(c *C) { + pk1 := testPrivKey1 + keyID := pk1.PublicKey().ID() + + got, err := mkms.keypairMgr.Get(keyID) + c.Check(got, IsNil) + c.Check(err, ErrorMatches, "cannot find key pair") + + err = mkms.keypairMgr.Put(pk1) + c.Assert(err, IsNil) + + got, err = mkms.keypairMgr.Get(keyID + "x") + c.Check(got, IsNil) + c.Check(err, ErrorMatches, "cannot find key pair") +} diff --git a/asserts/privkeys_for_test.go b/asserts/privkeys_for_test.go new file mode 100644 index 00000000..ec433f00 --- /dev/null +++ b/asserts/privkeys_for_test.go @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "encoding/base64" + + "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/sha3" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +// private keys to use in tests +var ( + // use a shorter key length here for test keys because otherwise + // they take too long to generate; + // the ones that care use pregenerated keys of the right length + // or use GenerateKey directly + testPrivKey0, _ = assertstest.GenerateKey(752) + testPrivKey1, testPrivKey1RSA = assertstest.GenerateKey(752) + testPrivKey2, _ = assertstest.GenerateKey(752) + + testPrivKey1SHA3_384 string +) + +func init() { + pkt := packet.NewRSAPrivateKey(asserts.V1FixedTimestamp, testPrivKey1RSA) + h := sha3.New384() + h.Write([]byte{0x1}) + err := pkt.PublicKey.Serialize(h) + if err != nil { + panic(err) + } + testPrivKey1SHA3_384 = base64.RawURLEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/asserts/repair.go b/asserts/repair.go new file mode 100644 index 00000000..27d765f7 --- /dev/null +++ b/asserts/repair.go @@ -0,0 +1,159 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +// Repair holds an repair assertion which allows running repair +// code to fixup broken systems. It can be limited by series and models. +type Repair struct { + assertionBase + + series []string + architectures []string + models []string + + id int + + disabled bool + timestamp time.Time +} + +// BrandID returns the brand identifier that signed this assertion. +func (r *Repair) BrandID() string { + return r.HeaderString("brand-id") +} + +// RepairID returns the sequential id of the repair. There +// should be a public place to look up details about the repair +// by brand-id and repair-id. +// (e.g. the snapcraft forum). +func (r *Repair) RepairID() int { + return r.id +} + +// Summary returns the mandatory summary description of the repair. +func (r *Repair) Summary() string { + return r.HeaderString("summary") +} + +// Architectures returns the architectures that this assertions applies to. +func (r *Repair) Architectures() []string { + return r.architectures +} + +// Series returns the series that this assertion is valid for. +func (r *Repair) Series() []string { + return r.series +} + +// Models returns the models that this assertion is valid for. +// It is a list of "brand-id/model-name" strings. +func (r *Repair) Models() []string { + return r.models +} + +// Disabled returns true if the repair has been disabled. +func (r *Repair) Disabled() bool { + return r.disabled +} + +// Timestamp returns the time when the repair was issued. +func (r *Repair) Timestamp() time.Time { + return r.timestamp +} + +// Implement further consistency checks. +func (r *Repair) checkConsistency(db RODatabase, acck *AccountKey) error { + // Do the cross-checks when this assertion is actually used, + // i.e. in the future repair code + + return nil +} + +// sanity +var _ consistencyChecker = (*Repair)(nil) + +// the repair-id can for now be a sequential number starting with 1 +var validRepairID = regexp.MustCompile("^[1-9][0-9]*$") + +func assembleRepair(assert assertionBase) (Assertion, error) { + err := checkAuthorityMatchesBrand(&assert) + if err != nil { + return nil, err + } + + repairID, err := checkStringMatches(assert.headers, "repair-id", validRepairID) + if err != nil { + return nil, err + } + id, err := strconv.Atoi(repairID) + if err != nil { + // given it matched it can likely only be too large + return nil, fmt.Errorf("repair-id too large: %s", repairID) + } + + summary, err := checkNotEmptyString(assert.headers, "summary") + if err != nil { + return nil, err + } + if strings.ContainsAny(summary, "\n\r") { + return nil, fmt.Errorf(`"summary" header cannot have newlines`) + } + + series, err := checkStringList(assert.headers, "series") + if err != nil { + return nil, err + } + models, err := checkStringList(assert.headers, "models") + if err != nil { + return nil, err + } + architectures, err := checkStringList(assert.headers, "architectures") + if err != nil { + return nil, err + } + + disabled, err := checkOptionalBool(assert.headers, "disabled") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &Repair{ + assertionBase: assert, + series: series, + architectures: architectures, + models: models, + id: id, + disabled: disabled, + timestamp: timestamp, + }, nil +} diff --git a/asserts/repair_test.go b/asserts/repair_test.go new file mode 100644 index 00000000..ffac7397 --- /dev/null +++ b/asserts/repair_test.go @@ -0,0 +1,183 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/snapcore/snapd/asserts" + . "gopkg.in/check.v1" +) + +var ( + _ = Suite(&repairSuite{}) +) + +type repairSuite struct { + modelsLine string + ts time.Time + tsLine string + + repairStr string +} + +const script = `#!/bin/sh +set -e +echo "Unpack embedded payload" +match=$(grep --text --line-number '^PAYLOAD:$' $0 | cut -d ':' -f 1) +payload_start=$((match + 1)) +# Using "base64" as its part of coreutils which should be available +# everywhere +tail -n +$payload_start $0 | base64 --decode - | tar -xzf - +# run embedded content +./hello +exit 0 +# payload generated with, may contain binary data +# printf '#!/bin/sh\necho hello from the inside\n' > hello +# chmod +x hello +# tar czf - hello | base64 - +PAYLOAD: +H4sIAJJt+FgAA+3STQrCMBDF8ax7ihEP0CkxyXn8iCZQE2jr/W11Iwi6KiL8f5u3mLd4i0mx76tZ +l86Cc0t2welrPu2c6awGr95bG4x26rw1oivveriN034QMfFSy6fet/uf2m7aQy7tmJp4TFXS8g5y +HupVphQllzGfYvPrkQAAAAAAAAAAAAAAAACAN3dTp9TNACgAAA== +` + +var repairExample = fmt.Sprintf("type: repair\n"+ + "authority-id: acme\n"+ + "brand-id: acme\n"+ + "summary: example repair\n"+ + "architectures:\n"+ + " - amd64\n"+ + " - arm64\n"+ + "repair-id: 42\n"+ + "series:\n"+ + " - 16\n"+ + "MODELSLINE"+ + "TSLINE"+ + "body-length: %v\n"+ + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij"+ + "\n\n"+ + script+"\n\n"+ + "AXNpZw==", len(script)) + +func (s *repairSuite) SetUpTest(c *C) { + s.modelsLine = "models:\n - acme/frobinator\n" + s.ts = time.Now().Truncate(time.Second).UTC() + s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n" + + s.repairStr = strings.Replace(repairExample, "MODELSLINE", s.modelsLine, 1) + s.repairStr = strings.Replace(s.repairStr, "TSLINE", s.tsLine, 1) +} + +func (s *repairSuite) TestDecodeOK(c *C) { + a, err := asserts.Decode([]byte(s.repairStr)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.RepairType) + repair := a.(*asserts.Repair) + c.Check(repair.Timestamp(), Equals, s.ts) + c.Check(repair.BrandID(), Equals, "acme") + c.Check(repair.RepairID(), Equals, 42) + c.Check(repair.Summary(), Equals, "example repair") + c.Check(repair.Series(), DeepEquals, []string{"16"}) + c.Check(repair.Architectures(), DeepEquals, []string{"amd64", "arm64"}) + c.Check(repair.Models(), DeepEquals, []string{"acme/frobinator"}) + c.Check(string(repair.Body()), Equals, script) +} + +const ( + repairErrPrefix = "assertion repair: " +) + +func (s *repairSuite) TestDisabled(c *C) { + disabledTests := []struct { + disabled, expectedErr string + dis bool + }{ + {"true", "", true}, + {"false", "", false}, + {"foo", `"disabled" header must be 'true' or 'false'`, false}, + } + + for _, test := range disabledTests { + repairStr := strings.Replace(repairExample, "MODELSLINE", fmt.Sprintf("disabled: %s\n", test.disabled), 1) + repairStr = strings.Replace(repairStr, "TSLINE", s.tsLine, 1) + + a, err := asserts.Decode([]byte(repairStr)) + if test.expectedErr != "" { + c.Check(err, ErrorMatches, repairErrPrefix+test.expectedErr) + } else { + c.Assert(err, IsNil) + repair := a.(*asserts.Repair) + c.Check(repair.Disabled(), Equals, test.dis) + } + } +} + +func (s *repairSuite) TestDecodeInvalid(c *C) { + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series:\n - 16\n", "series: \n", `"series" header must be a list of strings`}, + {"series:\n - 16\n", "series: something\n", `"series" header must be a list of strings`}, + {"architectures:\n - amd64\n - arm64\n", "architectures: foo\n", `"architectures" header must be a list of strings`}, + {"models:\n - acme/frobinator\n", "models: \n", `"models" header must be a list of strings`}, + {"models:\n - acme/frobinator\n", "models: something\n", `"models" header must be a list of strings`}, + {"repair-id: 42\n", "repair-id: no-number\n", `"repair-id" header contains invalid characters: "no-number"`}, + {"repair-id: 42\n", "repair-id: 0\n", `"repair-id" header contains invalid characters: "0"`}, + {"repair-id: 42\n", "repair-id: 01\n", `"repair-id" header contains invalid characters: "01"`}, + {"repair-id: 42\n", "repair-id: 99999999999999999999\n", `repair-id too large:.*`}, + {"brand-id: acme\n", "brand-id: brand-id-not-eq-authority-id\n", `authority-id and brand-id must match, repair assertions are expected to be signed by the brand: "acme" != "brand-id-not-eq-authority-id"`}, + {"summary: example repair\n", "", `"summary" header is mandatory`}, + {"summary: example repair\n", "summary: \n", `"summary" header should not be empty`}, + {"summary: example repair\n", "summary:\n multi\n line\n", `"summary" header cannot have newlines`}, + {s.tsLine, "", `"timestamp" header is mandatory`}, + {s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(s.repairStr, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, repairErrPrefix+test.expectedErr) + } +} + +// FIXME: move to a different layer later +func (s *repairSuite) TestRepairCanEmbeddScripts(c *C) { + a, err := asserts.Decode([]byte(s.repairStr)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.RepairType) + repair := a.(*asserts.Repair) + + tmpdir := c.MkDir() + repairScript := filepath.Join(tmpdir, "repair") + err = ioutil.WriteFile(repairScript, []byte(repair.Body()), 0755) + c.Assert(err, IsNil) + cmd := exec.Command(repairScript) + cmd.Dir = tmpdir + output, err := cmd.CombinedOutput() + c.Check(err, IsNil) + c.Check(string(output), Equals, `Unpack embedded payload +hello from the inside +`) +} diff --git a/asserts/signtool/sign.go b/asserts/signtool/sign.go new file mode 100644 index 00000000..2ef1bff0 --- /dev/null +++ b/asserts/signtool/sign.go @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +// Package signtool offers tooling to sign assertions. +package signtool + +import ( + "encoding/json" + "fmt" + + "github.com/snapcore/snapd/asserts" +) + +// Options specifies the complete input for signing an assertion. +type Options struct { + // KeyID specifies the key id of the key to use + KeyID string + + // Statement is used as input to construct the assertion + // it's a mapping encoded as JSON + // of the header fields of the assertion + // plus an optional pseudo-header "body" to specify + // the body of the assertion + Statement []byte +} + +// Sign produces the text of a signed assertion as specified by opts. +func Sign(opts *Options, keypairMgr asserts.KeypairManager) ([]byte, error) { + var headers map[string]interface{} + err := json.Unmarshal(opts.Statement, &headers) + if err != nil { + return nil, fmt.Errorf("cannot parse the assertion input as JSON: %v", err) + } + typCand, ok := headers["type"] + if !ok { + return nil, fmt.Errorf("missing assertion type header") + } + typStr, ok := typCand.(string) + if !ok { + return nil, fmt.Errorf("assertion type must be a string, not: %v", typCand) + } + typ := asserts.Type(typStr) + if typ == nil { + return nil, fmt.Errorf("invalid assertion type: %v", headers["type"]) + } + + var body []byte + if bodyCand, ok := headers["body"]; ok { + bodyStr, ok := bodyCand.(string) + if !ok { + return nil, fmt.Errorf("body if specified must be a string") + } + body = []byte(bodyStr) + delete(headers, "body") + } + + adb, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: keypairMgr, + }) + if err != nil { + return nil, err + } + + // TODO: teach Sign to cross check keyID and authority-id + // against an account-key + a, err := adb.Sign(typ, headers, body, opts.KeyID) + if err != nil { + return nil, err + } + + return asserts.Encode(a), nil +} diff --git a/asserts/signtool/sign_test.go b/asserts/signtool/sign_test.go new file mode 100644 index 00000000..007b7efc --- /dev/null +++ b/asserts/signtool/sign_test.go @@ -0,0 +1,179 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package signtool_test + +import ( + "encoding/json" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/signtool" +) + +func TestSigntool(t *testing.T) { TestingT(t) } + +type signSuite struct { + keypairMgr asserts.KeypairManager + testKeyID string +} + +var _ = Suite(&signSuite{}) + +func (s *signSuite) SetUpSuite(c *C) { + testKey, _ := assertstest.GenerateKey(752) + + s.keypairMgr = asserts.NewMemoryKeypairManager() + s.keypairMgr.Put(testKey) + s.testKeyID = testKey.PublicKey().ID() +} + +func expectedModelHeaders(a asserts.Assertion) map[string]interface{} { + m := map[string]interface{}{ + "type": "model", + "authority-id": "user-id1", + "series": "16", + "brand-id": "user-id1", + "model": "baz-3000", + "architecture": "amd64", + "gadget": "brand-gadget", + "kernel": "baz-linux", + "store": "brand-store", + "required-snaps": []interface{}{"foo", "bar"}, + "timestamp": "2015-11-25T20:00:00Z", + } + if a != nil { + m["sign-key-sha3-384"] = a.SignKeyID() + } + return m +} + +func exampleJSON(overrides map[string]interface{}) []byte { + m := expectedModelHeaders(nil) + for k, v := range overrides { + if v == nil { + delete(m, k) + } else { + m[k] = v + } + } + b, err := json.Marshal(m) + if err != nil { + panic(err) + } + return b +} + +func (s *signSuite) TestSignJSON(c *C) { + opts := signtool.Options{ + KeyID: s.testKeyID, + + Statement: exampleJSON(nil), + } + + assertText, err := signtool.Sign(&opts, s.keypairMgr) + c.Assert(err, IsNil) + + a, err := asserts.Decode(assertText) + c.Assert(err, IsNil) + + c.Check(a.Type(), Equals, asserts.ModelType) + c.Check(a.Revision(), Equals, 0) + expectedHeaders := expectedModelHeaders(a) + c.Check(a.Headers(), DeepEquals, expectedHeaders) + + for n, v := range a.Headers() { + c.Check(v, DeepEquals, expectedHeaders[n], Commentf(n)) + } + + c.Check(a.Body(), IsNil) +} + +func (s *signSuite) TestSignJSONWithBodyAndRevision(c *C) { + statement := exampleJSON(map[string]interface{}{ + "body": "BODY", + "revision": "11", + }) + opts := signtool.Options{ + KeyID: s.testKeyID, + + Statement: statement, + } + + assertText, err := signtool.Sign(&opts, s.keypairMgr) + c.Assert(err, IsNil) + + a, err := asserts.Decode(assertText) + c.Assert(err, IsNil) + + c.Check(a.Type(), Equals, asserts.ModelType) + c.Check(a.Revision(), Equals, 11) + + expectedHeaders := expectedModelHeaders(a) + expectedHeaders["revision"] = "11" + expectedHeaders["body-length"] = "4" + + c.Check(a.Headers(), DeepEquals, expectedHeaders) + + c.Check(a.Body(), DeepEquals, []byte("BODY")) +} + +func (s *signSuite) TestSignErrors(c *C) { + opts := signtool.Options{ + KeyID: s.testKeyID, + } + + emptyList := []interface{}{} + + tests := []struct { + expError string + brokenStatement []byte + }{ + {`cannot parse the assertion input as JSON:.*`, + []byte("\x00"), + }, + {`invalid assertion type: what`, + exampleJSON(map[string]interface{}{"type": "what"}), + }, + {`assertion type must be a string, not: \[\]`, + exampleJSON(map[string]interface{}{"type": emptyList}), + }, + {`missing assertion type header`, + exampleJSON(map[string]interface{}{"type": nil}), + }, + {"revision should be positive: -10", + exampleJSON(map[string]interface{}{"revision": "-10"})}, + {`"authority-id" header is mandatory`, + exampleJSON(map[string]interface{}{"authority-id": nil})}, + {`body if specified must be a string`, + exampleJSON(map[string]interface{}{"body": emptyList})}, + } + + for _, t := range tests { + fresh := opts + + fresh.Statement = t.brokenStatement + + _, err := signtool.Sign(&fresh, s.keypairMgr) + c.Check(err, ErrorMatches, t.expError) + } +} diff --git a/asserts/snap_asserts.go b/asserts/snap_asserts.go new file mode 100644 index 00000000..3575667e --- /dev/null +++ b/asserts/snap_asserts.go @@ -0,0 +1,931 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "bytes" + "crypto" + "fmt" + "regexp" + "time" + + _ "golang.org/x/crypto/sha3" // expected for digests + + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +// SnapDeclaration holds a snap-declaration assertion, declaring a +// snap binding its identifying snap-id to a name, asserting its +// publisher and its other properties. +type SnapDeclaration struct { + assertionBase + refreshControl []string + plugRules map[string]*PlugRule + slotRules map[string]*SlotRule + autoAliases []string + aliases map[string]string + timestamp time.Time +} + +// Series returns the series for which the snap is being declared. +func (snapdcl *SnapDeclaration) Series() string { + return snapdcl.HeaderString("series") +} + +// SnapID returns the snap id of the declared snap. +func (snapdcl *SnapDeclaration) SnapID() string { + return snapdcl.HeaderString("snap-id") +} + +// SnapName returns the declared snap name. +func (snapdcl *SnapDeclaration) SnapName() string { + return snapdcl.HeaderString("snap-name") +} + +// PublisherID returns the identifier of the publisher of the declared snap. +func (snapdcl *SnapDeclaration) PublisherID() string { + return snapdcl.HeaderString("publisher-id") +} + +// Timestamp returns the time when the snap-declaration was issued. +func (snapdcl *SnapDeclaration) Timestamp() time.Time { + return snapdcl.timestamp +} + +// RefreshControl returns the ids of snaps whose updates are controlled by this declaration. +func (snapdcl *SnapDeclaration) RefreshControl() []string { + return snapdcl.refreshControl +} + +// PlugRule returns the plug-side rule about the given interface if one was included in the plugs stanza of the declaration, otherwise it returns nil. +func (snapdcl *SnapDeclaration) PlugRule(interfaceName string) *PlugRule { + return snapdcl.plugRules[interfaceName] +} + +// SlotRule returns the slot-side rule about the given interface if one was included in the slots stanza of the declaration, otherwise it returns nil. +func (snapdcl *SnapDeclaration) SlotRule(interfaceName string) *SlotRule { + return snapdcl.slotRules[interfaceName] +} + +// AutoAliases returns the optional auto-aliases granted to this snap. +// XXX: deprecated, will go away +func (snapdcl *SnapDeclaration) AutoAliases() []string { + return snapdcl.autoAliases +} + +// Aliases returns the optional explicit aliases granted to this snap. +func (snapdcl *SnapDeclaration) Aliases() map[string]string { + return snapdcl.aliases +} + +// Implement further consistency checks. +func (snapdcl *SnapDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error { + if !db.IsTrustedAccount(snapdcl.AuthorityID()) { + return fmt.Errorf("snap-declaration assertion for %q (id %q) is not signed by a directly trusted authority: %s", snapdcl.SnapName(), snapdcl.SnapID(), snapdcl.AuthorityID()) + } + _, err := db.Find(AccountType, map[string]string{ + "account-id": snapdcl.PublisherID(), + }) + if IsNotFound(err) { + return fmt.Errorf("snap-declaration assertion for %q (id %q) does not have a matching account assertion for the publisher %q", snapdcl.SnapName(), snapdcl.SnapID(), snapdcl.PublisherID()) + } + if err != nil { + return err + } + + return nil +} + +// sanity +var _ consistencyChecker = (*SnapDeclaration)(nil) + +// Prerequisites returns references to this snap-declaration's prerequisite assertions. +func (snapdcl *SnapDeclaration) Prerequisites() []*Ref { + return []*Ref{ + {Type: AccountType, PrimaryKey: []string{snapdcl.PublisherID()}}, + } +} + +func compilePlugRules(plugs map[string]interface{}, compiled func(iface string, plugRule *PlugRule)) error { + for iface, rule := range plugs { + plugRule, err := compilePlugRule(iface, rule) + if err != nil { + return err + } + compiled(iface, plugRule) + } + return nil +} + +func compileSlotRules(slots map[string]interface{}, compiled func(iface string, slotRule *SlotRule)) error { + for iface, rule := range slots { + slotRule, err := compileSlotRule(iface, rule) + if err != nil { + return err + } + compiled(iface, slotRule) + } + return nil +} + +func snapDeclarationFormatAnalyze(headers map[string]interface{}, body []byte) (formatnum int, err error) { + _, plugsOk := headers["plugs"] + _, slotsOk := headers["slots"] + if !(plugsOk || slotsOk) { + return 0, nil + } + formatnum = 1 + + plugs, err := checkMap(headers, "plugs") + if err != nil { + return 0, err + } + err = compilePlugRules(plugs, func(_ string, rule *PlugRule) { + if rule.feature(dollarAttrConstraintsFeature) { + formatnum = 2 + } + }) + if err != nil { + return 0, err + } + + slots, err := checkMap(headers, "slots") + if err != nil { + return 0, err + } + err = compileSlotRules(slots, func(_ string, rule *SlotRule) { + if rule.feature(dollarAttrConstraintsFeature) { + formatnum = 2 + } + }) + if err != nil { + return 0, err + } + + return formatnum, nil +} + +var ( + validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") + validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") +) + +func checkAliases(headers map[string]interface{}) (map[string]string, error) { + value, ok := headers["aliases"] + if !ok { + return nil, nil + } + aliasList, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf(`"aliases" header must be a list of alias maps`) + } + if len(aliasList) == 0 { + return nil, nil + } + + aliasMap := make(map[string]string, len(aliasList)) + for i, item := range aliasList { + aliasItem, ok := item.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf(`"aliases" header must be a list of alias maps`) + } + + what := fmt.Sprintf(`in "aliases" item %d`, i+1) + name, err := checkStringMatchesWhat(aliasItem, "name", what, validAlias) + if err != nil { + return nil, err + } + + what = fmt.Sprintf(`for alias %q`, name) + target, err := checkStringMatchesWhat(aliasItem, "target", what, validAppName) + if err != nil { + return nil, err + } + + if _, ok := aliasMap[name]; ok { + return nil, fmt.Errorf(`duplicated definition in "aliases" for alias %q`, name) + } + + aliasMap[name] = target + } + + return aliasMap, nil +} + +func assembleSnapDeclaration(assert assertionBase) (Assertion, error) { + _, err := checkExistsString(assert.headers, "snap-name") + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "publisher-id") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + var refControl []string + var plugRules map[string]*PlugRule + var slotRules map[string]*SlotRule + + refControl, err = checkStringList(assert.headers, "refresh-control") + if err != nil { + return nil, err + } + + plugs, err := checkMap(assert.headers, "plugs") + if err != nil { + return nil, err + } + if plugs != nil { + plugRules = make(map[string]*PlugRule, len(plugs)) + err := compilePlugRules(plugs, func(iface string, rule *PlugRule) { + plugRules[iface] = rule + }) + if err != nil { + return nil, err + } + } + + slots, err := checkMap(assert.headers, "slots") + if err != nil { + return nil, err + } + if slots != nil { + slotRules = make(map[string]*SlotRule, len(slots)) + err := compileSlotRules(slots, func(iface string, rule *SlotRule) { + slotRules[iface] = rule + }) + if err != nil { + return nil, err + } + } + + // XXX: depracated, will go away later + autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", validAlias) + if err != nil { + return nil, err + } + + aliases, err := checkAliases(assert.headers) + if err != nil { + return nil, err + } + + return &SnapDeclaration{ + assertionBase: assert, + refreshControl: refControl, + plugRules: plugRules, + slotRules: slotRules, + autoAliases: autoAliases, + aliases: aliases, + timestamp: timestamp, + }, nil +} + +// SnapFileSHA3_384 computes the SHA3-384 digest of the given snap file. +// It also returns its size. +func SnapFileSHA3_384(snapPath string) (digest string, size uint64, err error) { + sha3_384Dgst, size, err := osutil.FileDigest(snapPath, crypto.SHA3_384) + if err != nil { + return "", 0, fmt.Errorf("cannot compute snap %q digest: %v", snapPath, err) + } + + sha3_384, err := EncodeDigest(crypto.SHA3_384, sha3_384Dgst) + if err != nil { + return "", 0, fmt.Errorf("cannot encode snap %q digest: %v", snapPath, err) + } + return sha3_384, size, nil +} + +// SnapBuild holds a snap-build assertion, asserting the properties of a snap +// at the time it was built by the developer. +type SnapBuild struct { + assertionBase + size uint64 + timestamp time.Time +} + +// SnapSHA3_384 returns the SHA3-384 digest of the snap. +func (snapbld *SnapBuild) SnapSHA3_384() string { + return snapbld.HeaderString("snap-sha3-384") +} + +// SnapID returns the snap id of the snap. +func (snapbld *SnapBuild) SnapID() string { + return snapbld.HeaderString("snap-id") +} + +// SnapSize returns the size of the snap. +func (snapbld *SnapBuild) SnapSize() uint64 { + return snapbld.size +} + +// Grade returns the grade of the snap: devel|stable +func (snapbld *SnapBuild) Grade() string { + return snapbld.HeaderString("grade") +} + +// Timestamp returns the time when the snap-build assertion was created. +func (snapbld *SnapBuild) Timestamp() time.Time { + return snapbld.timestamp +} + +func assembleSnapBuild(assert assertionBase) (Assertion, error) { + _, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384) + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "snap-id") + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "grade") + if err != nil { + return nil, err + } + + size, err := checkUint(assert.headers, "snap-size", 64) + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + // ignore extra headers and non-empty body for future compatibility + return &SnapBuild{ + assertionBase: assert, + size: size, + timestamp: timestamp, + }, nil +} + +// SnapRevision holds a snap-revision assertion, which is a statement by the +// store acknowledging the receipt of a build of a snap and labeling it with a +// snap revision. +type SnapRevision struct { + assertionBase + snapSize uint64 + snapRevision int + timestamp time.Time +} + +// SnapSHA3_384 returns the SHA3-384 digest of the snap. +func (snaprev *SnapRevision) SnapSHA3_384() string { + return snaprev.HeaderString("snap-sha3-384") +} + +// SnapID returns the snap id of the snap. +func (snaprev *SnapRevision) SnapID() string { + return snaprev.HeaderString("snap-id") +} + +// SnapSize returns the size in bytes of the snap submitted to the store. +func (snaprev *SnapRevision) SnapSize() uint64 { + return snaprev.snapSize +} + +// SnapRevision returns the revision assigned to this build of the snap. +func (snaprev *SnapRevision) SnapRevision() int { + return snaprev.snapRevision +} + +// DeveloperID returns the id of the developer that submitted this build of the +// snap. +func (snaprev *SnapRevision) DeveloperID() string { + return snaprev.HeaderString("developer-id") +} + +// Timestamp returns the time when the snap-revision was issued. +func (snaprev *SnapRevision) Timestamp() time.Time { + return snaprev.timestamp +} + +// Implement further consistency checks. +func (snaprev *SnapRevision) checkConsistency(db RODatabase, acck *AccountKey) error { + // TODO: expand this to consider other stores signing on their own + if !db.IsTrustedAccount(snaprev.AuthorityID()) { + return fmt.Errorf("snap-revision assertion for snap id %q is not signed by a store: %s", snaprev.SnapID(), snaprev.AuthorityID()) + } + _, err := db.Find(AccountType, map[string]string{ + "account-id": snaprev.DeveloperID(), + }) + if IsNotFound(err) { + return fmt.Errorf("snap-revision assertion for snap id %q does not have a matching account assertion for the developer %q", snaprev.SnapID(), snaprev.DeveloperID()) + } + if err != nil { + return err + } + _, err = db.Find(SnapDeclarationType, map[string]string{ + // XXX: mediate getting current series through some context object? this gets the job done for now + "series": release.Series, + "snap-id": snaprev.SnapID(), + }) + if IsNotFound(err) { + return fmt.Errorf("snap-revision assertion for snap id %q does not have a matching snap-declaration assertion", snaprev.SnapID()) + } + if err != nil { + return err + } + return nil +} + +// sanity +var _ consistencyChecker = (*SnapRevision)(nil) + +// Prerequisites returns references to this snap-revision's prerequisite assertions. +func (snaprev *SnapRevision) Prerequisites() []*Ref { + return []*Ref{ + // XXX: mediate getting current series through some context object? this gets the job done for now + {Type: SnapDeclarationType, PrimaryKey: []string{release.Series, snaprev.SnapID()}}, + {Type: AccountType, PrimaryKey: []string{snaprev.DeveloperID()}}, + } +} + +func assembleSnapRevision(assert assertionBase) (Assertion, error) { + _, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384) + if err != nil { + return nil, err + } + + _, err = checkNotEmptyString(assert.headers, "snap-id") + if err != nil { + return nil, err + } + + snapSize, err := checkUint(assert.headers, "snap-size", 64) + if err != nil { + return nil, err + } + + snapRevision, err := checkInt(assert.headers, "snap-revision") + if err != nil { + return nil, err + } + if snapRevision < 1 { + return nil, fmt.Errorf(`"snap-revision" header must be >=1: %d`, snapRevision) + } + + _, err = checkNotEmptyString(assert.headers, "developer-id") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &SnapRevision{ + assertionBase: assert, + snapSize: snapSize, + snapRevision: snapRevision, + timestamp: timestamp, + }, nil +} + +// Validation holds a validation assertion, describing that a combination of +// (snap-id, approved-snap-id, approved-revision) has been validated for +// the series, meaning updating to that revision of approved-snap-id +// has been approved by the owner of the gating snap with snap-id. +type Validation struct { + assertionBase + revoked bool + timestamp time.Time + approvedSnapRevision int +} + +// Series returns the series for which the validation holds. +func (validation *Validation) Series() string { + return validation.HeaderString("series") +} + +// SnapID returns the ID of the gating snap. +func (validation *Validation) SnapID() string { + return validation.HeaderString("snap-id") +} + +// ApprovedSnapID returns the ID of the gated snap. +func (validation *Validation) ApprovedSnapID() string { + return validation.HeaderString("approved-snap-id") +} + +// ApprovedSnapRevision returns the approved revision of the gated snap. +func (validation *Validation) ApprovedSnapRevision() int { + return validation.approvedSnapRevision +} + +// Revoked returns true if the validation has been revoked. +func (validation *Validation) Revoked() bool { + return validation.revoked +} + +// Timestamp returns the time when the validation was issued. +func (validation *Validation) Timestamp() time.Time { + return validation.timestamp +} + +// Implement further consistency checks. +func (validation *Validation) checkConsistency(db RODatabase, acck *AccountKey) error { + _, err := db.Find(SnapDeclarationType, map[string]string{ + "series": validation.Series(), + "snap-id": validation.ApprovedSnapID(), + }) + if IsNotFound(err) { + return fmt.Errorf("validation assertion by snap-id %q does not have a matching snap-declaration assertion for approved-snap-id %q", validation.SnapID(), validation.ApprovedSnapID()) + } + if err != nil { + return err + } + a, err := db.Find(SnapDeclarationType, map[string]string{ + "series": validation.Series(), + "snap-id": validation.SnapID(), + }) + if IsNotFound(err) { + return fmt.Errorf("validation assertion by snap-id %q does not have a matching snap-declaration assertion", validation.SnapID()) + } + if err != nil { + return err + } + + gatingDecl := a.(*SnapDeclaration) + if gatingDecl.PublisherID() != validation.AuthorityID() { + return fmt.Errorf("validation assertion by snap %q (id %q) not signed by its publisher", gatingDecl.SnapName(), validation.SnapID()) + } + + return nil +} + +// sanity +var _ consistencyChecker = (*Validation)(nil) + +// Prerequisites returns references to this validation's prerequisite assertions. +func (validation *Validation) Prerequisites() []*Ref { + return []*Ref{ + {Type: SnapDeclarationType, PrimaryKey: []string{validation.Series(), validation.SnapID()}}, + {Type: SnapDeclarationType, PrimaryKey: []string{validation.Series(), validation.ApprovedSnapID()}}, + } +} + +func assembleValidation(assert assertionBase) (Assertion, error) { + approvedSnapRevision, err := checkInt(assert.headers, "approved-snap-revision") + if err != nil { + return nil, err + } + if approvedSnapRevision < 1 { + return nil, fmt.Errorf(`"approved-snap-revision" header must be >=1: %d`, approvedSnapRevision) + } + + revoked, err := checkOptionalBool(assert.headers, "revoked") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &Validation{ + assertionBase: assert, + revoked: revoked, + timestamp: timestamp, + approvedSnapRevision: approvedSnapRevision, + }, nil +} + +// BaseDeclaration holds a base-declaration assertion, declaring the +// policies (to start with interface ones) applying to all snaps of +// a series. +type BaseDeclaration struct { + assertionBase + plugRules map[string]*PlugRule + slotRules map[string]*SlotRule + timestamp time.Time +} + +// Series returns the series whose snaps are governed by the declaration. +func (basedcl *BaseDeclaration) Series() string { + return basedcl.HeaderString("series") +} + +// Timestamp returns the time when the base-declaration was issued. +func (basedcl *BaseDeclaration) Timestamp() time.Time { + return basedcl.timestamp +} + +// PlugRule returns the plug-side rule about the given interface if one was included in the plugs stanza of the declaration, otherwise it returns nil. +func (basedcl *BaseDeclaration) PlugRule(interfaceName string) *PlugRule { + return basedcl.plugRules[interfaceName] +} + +// SlotRule returns the slot-side rule about the given interface if one was included in the slots stanza of the declaration, otherwise it returns nil. +func (basedcl *BaseDeclaration) SlotRule(interfaceName string) *SlotRule { + return basedcl.slotRules[interfaceName] +} + +// Implement further consistency checks. +func (basedcl *BaseDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error { + // XXX: not signed or stored yet in a db, but being ready for that + if !db.IsTrustedAccount(basedcl.AuthorityID()) { + return fmt.Errorf("base-declaration assertion for series %s is not signed by a directly trusted authority: %s", basedcl.Series(), basedcl.AuthorityID()) + } + return nil +} + +// sanity +var _ consistencyChecker = (*BaseDeclaration)(nil) + +func assembleBaseDeclaration(assert assertionBase) (Assertion, error) { + var plugRules map[string]*PlugRule + plugs, err := checkMap(assert.headers, "plugs") + if err != nil { + return nil, err + } + if plugs != nil { + plugRules = make(map[string]*PlugRule, len(plugs)) + err := compilePlugRules(plugs, func(iface string, rule *PlugRule) { + plugRules[iface] = rule + }) + if err != nil { + return nil, err + } + } + + var slotRules map[string]*SlotRule + slots, err := checkMap(assert.headers, "slots") + if err != nil { + return nil, err + } + if slots != nil { + slotRules = make(map[string]*SlotRule, len(slots)) + err := compileSlotRules(slots, func(iface string, rule *SlotRule) { + slotRules[iface] = rule + }) + if err != nil { + return nil, err + } + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &BaseDeclaration{ + assertionBase: assert, + plugRules: plugRules, + slotRules: slotRules, + timestamp: timestamp, + }, nil +} + +var builtinBaseDeclaration *BaseDeclaration + +// BuiltinBaseDeclaration exposes the initialized builtin base-declaration assertion. This is used by overlord/assertstate, other code should use assertstate.BaseDeclaration. +func BuiltinBaseDeclaration() *BaseDeclaration { + return builtinBaseDeclaration +} + +var ( + builtinBaseDeclarationCheckOrder = []string{"type", "authority-id", "series"} + builtinBaseDeclarationExpectedHeaders = map[string]interface{}{ + "type": "base-declaration", + "authority-id": "canonical", + "series": release.Series, + } +) + +// InitBuiltinBaseDeclaration initializes the builtin base-declaration based on headers (or resets it if headers is nil). +func InitBuiltinBaseDeclaration(headers []byte) error { + if headers == nil { + builtinBaseDeclaration = nil + return nil + } + trimmed := bytes.TrimSpace(headers) + h, err := parseHeaders(trimmed) + if err != nil { + return err + } + for _, name := range builtinBaseDeclarationCheckOrder { + expected := builtinBaseDeclarationExpectedHeaders[name] + if h[name] != expected { + return fmt.Errorf("the builtin base-declaration %q header is not set to expected value %q", name, expected) + } + } + revision, err := checkRevision(h) + if err != nil { + return fmt.Errorf("cannot assemble the builtin-base declaration: %v", err) + } + h["timestamp"] = time.Now().UTC().Format(time.RFC3339) + a, err := assembleBaseDeclaration(assertionBase{ + headers: h, + body: nil, + revision: revision, + content: trimmed, + signature: []byte("$builtin"), + }) + if err != nil { + return fmt.Errorf("cannot assemble the builtin base-declaration: %v", err) + } + builtinBaseDeclaration = a.(*BaseDeclaration) + return nil +} + +type dateRange struct { + Since time.Time + Until time.Time +} + +// SnapDeveloper holds a snap-developer assertion, defining the developers who +// can collaborate on a snap while it's owned by a specific publisher. +// +// The primary key (snap-id, publisher-id) allows a snap to have many +// snap-developer assertions, e.g. to allow a future publisher's collaborations +// to be defined before the snap is transferred. However only the +// snap-developer for the current publisher (the snap-declaration publisher-id) +// is relevant to a device. +type SnapDeveloper struct { + assertionBase + developerRanges map[string][]*dateRange +} + +// SnapID returns the snap id of the snap. +func (snapdev *SnapDeveloper) SnapID() string { + return snapdev.HeaderString("snap-id") +} + +// PublisherID returns the publisher's account id. +func (snapdev *SnapDeveloper) PublisherID() string { + return snapdev.HeaderString("publisher-id") +} + +func (snapdev *SnapDeveloper) checkConsistency(db RODatabase, acck *AccountKey) error { + // Check authority is the publisher or trusted. + authorityID := snapdev.AuthorityID() + publisherID := snapdev.PublisherID() + if !db.IsTrustedAccount(authorityID) && (publisherID != authorityID) { + return fmt.Errorf("snap-developer must be signed by the publisher or a trusted authority but got authority %q and publisher %q", authorityID, publisherID) + } + + // Check snap-declaration for the snap-id exists for the series. + // Note: the current publisher is irrelevant here because this assertion + // may be for a future publisher. + _, err := db.Find(SnapDeclarationType, map[string]string{ + // XXX: mediate getting current series through some context object? this gets the job done for now + "series": release.Series, + "snap-id": snapdev.SnapID(), + }) + if err != nil { + if IsNotFound(err) { + return fmt.Errorf("snap-developer assertion for snap id %q does not have a matching snap-declaration assertion", snapdev.SnapID()) + } + return err + } + + // check there's an account for the publisher-id + _, err = db.Find(AccountType, map[string]string{"account-id": publisherID}) + if err != nil { + if IsNotFound(err) { + return fmt.Errorf("snap-developer assertion for snap-id %q does not have a matching account assertion for the publisher %q", snapdev.SnapID(), publisherID) + } + return err + } + + // check there's an account for each developer + for developerID := range snapdev.developerRanges { + if developerID == publisherID { + continue + } + _, err = db.Find(AccountType, map[string]string{"account-id": developerID}) + if err != nil { + if IsNotFound(err) { + return fmt.Errorf("snap-developer assertion for snap-id %q does not have a matching account assertion for the developer %q", snapdev.SnapID(), developerID) + } + return err + } + } + + return nil +} + +// sanity +var _ consistencyChecker = (*SnapDeveloper)(nil) + +// Prerequisites returns references to this snap-developer's prerequisite assertions. +func (snapdev *SnapDeveloper) Prerequisites() []*Ref { + // Capacity for the snap-declaration, the publisher and all developers. + refs := make([]*Ref, 0, 2+len(snapdev.developerRanges)) + + // snap-declaration + // XXX: mediate getting current series through some context object? this gets the job done for now + refs = append(refs, &Ref{SnapDeclarationType, []string{release.Series, snapdev.SnapID()}}) + + // the publisher and developers + publisherID := snapdev.PublisherID() + refs = append(refs, &Ref{AccountType, []string{publisherID}}) + for developerID := range snapdev.developerRanges { + if developerID != publisherID { + refs = append(refs, &Ref{AccountType, []string{developerID}}) + } + } + + return refs +} + +func assembleSnapDeveloper(assert assertionBase) (Assertion, error) { + developerRanges, err := checkDevelopers(assert.headers) + if err != nil { + return nil, err + } + + return &SnapDeveloper{ + assertionBase: assert, + developerRanges: developerRanges, + }, nil +} + +func checkDevelopers(headers map[string]interface{}) (map[string][]*dateRange, error) { + value, ok := headers["developers"] + if !ok { + return nil, nil + } + developers, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf(`"developers" must be a list of developer maps`) + } + if len(developers) == 0 { + return nil, nil + } + + // Used to check for a developer with revoking and non-revoking items. + // No entry means developer not yet seen, false means seen but not revoked, + // true means seen and revoked. + revocationStatus := map[string]bool{} + + developerRanges := make(map[string][]*dateRange) + for i, item := range developers { + developer, ok := item.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf(`"developers" must be a list of developer maps`) + } + + what := fmt.Sprintf(`in "developers" item %d`, i+1) + accountID, err := checkStringMatchesWhat(developer, "developer-id", what, validAccountID) + if err != nil { + return nil, err + } + + what = fmt.Sprintf(`in "developers" item %d for developer %q`, i+1, accountID) + since, err := checkRFC3339DateWhat(developer, "since", what) + if err != nil { + return nil, err + } + until, err := checkRFC3339DateWithDefaultWhat(developer, "until", what, time.Time{}) + if err != nil { + return nil, err + } + if !until.IsZero() && since.After(until) { + return nil, fmt.Errorf(`"since" %s must be less than or equal to "until"`, what) + } + + // Track/check for revocation conflicts. + revoked := since.Equal(until) + previouslyRevoked, ok := revocationStatus[accountID] + if !ok { + revocationStatus[accountID] = revoked + } else if previouslyRevoked || revoked { + return nil, fmt.Errorf(`revocation for developer %q must be standalone but found other "developers" items`, accountID) + } + + developerRanges[accountID] = append(developerRanges[accountID], &dateRange{since, until}) + } + + return developerRanges, nil +} diff --git a/asserts/snap_asserts_test.go b/asserts/snap_asserts_test.go new file mode 100644 index 00000000..60457fd1 --- /dev/null +++ b/asserts/snap_asserts_test.go @@ -0,0 +1,1776 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "encoding/base64" + "io/ioutil" + "path/filepath" + "sort" + "strings" + "time" + + "golang.org/x/crypto/sha3" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +var ( + _ = Suite(&snapDeclSuite{}) + _ = Suite(&snapFileDigestSuite{}) + _ = Suite(&snapBuildSuite{}) + _ = Suite(&snapRevSuite{}) + _ = Suite(&validationSuite{}) + _ = Suite(&baseDeclSuite{}) + _ = Suite(&snapDevSuite{}) +) + +type snapDeclSuite struct { + ts time.Time + tsLine string +} + +func (sds *snapDeclSuite) SetUpSuite(c *C) { + sds.ts = time.Now().Truncate(time.Second).UTC() + sds.tsLine = "timestamp: " + sds.ts.Format(time.RFC3339) + "\n" +} + +func (sds *snapDeclSuite) TestDecodeOK(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + "refresh-control:\n - foo\n - bar\n" + + "auto-aliases:\n - cmd1\n - cmd_2\n - Cmd-3\n - CMD.4\n" + + sds.tsLine + + `aliases: + - + name: cmd1 + target: cmd-1 + - + name: cmd_2 + target: cmd-2 + - + name: Cmd-3 + target: cmd-3 + - + name: CMD.4 + target: cmd-4 +` + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapDeclarationType) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.AuthorityID(), Equals, "canonical") + c.Check(snapDecl.Timestamp(), Equals, sds.ts) + c.Check(snapDecl.Series(), Equals, "16") + c.Check(snapDecl.SnapID(), Equals, "snap-id-1") + c.Check(snapDecl.SnapName(), Equals, "first") + c.Check(snapDecl.PublisherID(), Equals, "dev-id1") + c.Check(snapDecl.RefreshControl(), DeepEquals, []string{"foo", "bar"}) + c.Check(snapDecl.AutoAliases(), DeepEquals, []string{"cmd1", "cmd_2", "Cmd-3", "CMD.4"}) + c.Check(snapDecl.Aliases(), DeepEquals, map[string]string{ + "cmd1": "cmd-1", + "cmd_2": "cmd-2", + "Cmd-3": "cmd-3", + "CMD.4": "cmd-4", + }) +} + +func (sds *snapDeclSuite) TestEmptySnapName(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: \n" + + "publisher-id: dev-id1\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.SnapName(), Equals, "") +} + +func (sds *snapDeclSuite) TestMissingRefreshControlAutoAliases(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: \n" + + "publisher-id: dev-id1\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.RefreshControl(), HasLen, 0) + c.Check(snapDecl.AutoAliases(), HasLen, 0) +} + +const ( + snapDeclErrPrefix = "assertion snap-declaration: " +) + +func (sds *snapDeclSuite) TestDecodeInvalid(c *C) { + aliases := `aliases: + - + name: cmd_1 + target: cmd-1 +` + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + "refresh-control:\n - foo\n - bar\n" + + "auto-aliases:\n - cmd1\n - cmd2\n" + + aliases + + "plugs:\n interface1: true\n" + + "slots:\n interface2: true\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {"snap-name: first\n", "", `"snap-name" header is mandatory`}, + {"publisher-id: dev-id1\n", "", `"publisher-id" header is mandatory`}, + {"publisher-id: dev-id1\n", "publisher-id: \n", `"publisher-id" header should not be empty`}, + {"refresh-control:\n - foo\n - bar\n", "refresh-control: foo\n", `"refresh-control" header must be a list of strings`}, + {"refresh-control:\n - foo\n - bar\n", "refresh-control:\n -\n - nested\n", `"refresh-control" header must be a list of strings`}, + {"plugs:\n interface1: true\n", "plugs: \n", `"plugs" header must be a map`}, + {"plugs:\n interface1: true\n", "plugs:\n intf1:\n foo: bar\n", `plug rule for interface "intf1" must specify at least one of.*`}, + {"slots:\n interface2: true\n", "slots: \n", `"slots" header must be a map`}, + {"slots:\n interface2: true\n", "slots:\n intf1:\n foo: bar\n", `slot rule for interface "intf1" must specify at least one of.*`}, + {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases: cmd0\n", `"auto-aliases" header must be a list of strings`}, + {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases:\n -\n - nested\n", `"auto-aliases" header must be a list of strings`}, + {"auto-aliases:\n - cmd1\n - cmd2\n", "auto-aliases:\n - _cmd-1\n - cmd2\n", `"auto-aliases" header contains an invalid element: "_cmd-1"`}, + {aliases, "aliases: cmd0\n", `"aliases" header must be a list of alias maps`}, + {aliases, "aliases:\n - cmd1\n", `"aliases" header must be a list of alias maps`}, + {"name: cmd_1\n", "name: .cmd1\n", `"name" in "aliases" item 1 contains invalid characters: ".cmd1"`}, + {"target: cmd-1\n", "target: -cmd-1\n", `"target" for alias "cmd_1" contains invalid characters: "-cmd-1"`}, + {aliases, aliases + " -\n name: cmd_1\n target: foo\n", `duplicated definition in "aliases" for alias "cmd_1"`}, + {sds.tsLine, "", `"timestamp" header is mandatory`}, + {sds.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {sds.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapDeclErrPrefix+test.expectedErr) + } + +} + +func (sds *snapDeclSuite) TestDecodePlugsAndSlots(c *C) { + encoded := `type: snap-declaration +format: 1 +authority-id: canonical +series: 16 +snap-id: snap-id-1 +snap-name: first +publisher-id: dev-id1 +plugs: + interface1: + deny-installation: false + allow-auto-connection: + slot-snap-type: + - app + slot-publisher-id: + - acme + slot-attributes: + a1: /foo/.* + plug-attributes: + b1: B1 + deny-auto-connection: + slot-attributes: + a1: !A1 + plug-attributes: + b1: !B1 + interface2: + allow-installation: true + allow-connection: + plug-attributes: + a2: A2 + slot-attributes: + b2: B2 + deny-connection: + slot-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + a2: !A2 + slot-attributes: + b2: !B2 +slots: + interface3: + deny-installation: false + allow-auto-connection: + plug-snap-type: + - app + plug-publisher-id: + - acme + slot-attributes: + c1: /foo/.* + plug-attributes: + d1: C1 + deny-auto-connection: + slot-attributes: + c1: !C1 + plug-attributes: + d1: !D1 + interface4: + allow-connection: + plug-attributes: + c2: C2 + slot-attributes: + d2: D2 + deny-connection: + plug-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + c2: !D2 + slot-attributes: + d2: !D2 + allow-installation: + slot-snap-type: + - app + slot-attributes: + e1: E1 +TSLINE +body-length: 0 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw==` + encoded = strings.Replace(encoded, "TSLINE\n", sds.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.SupportedFormat(), Equals, true) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.Series(), Equals, "16") + c.Check(snapDecl.SnapID(), Equals, "snap-id-1") + + c.Check(snapDecl.PlugRule("interfaceX"), IsNil) + c.Check(snapDecl.SlotRule("interfaceX"), IsNil) + + plugRule1 := snapDecl.PlugRule("interface1") + c.Assert(plugRule1, NotNil) + c.Assert(plugRule1.DenyInstallation, HasLen, 1) + c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) + c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) + c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + plugRule2 := snapDecl.PlugRule("interface2") + c.Assert(plugRule2, NotNil) + c.Assert(plugRule2.AllowInstallation, HasLen, 1) + c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(plugRule2.AllowConnection, HasLen, 1) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Assert(plugRule2.DenyConnection, HasLen, 1) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + + slotRule3 := snapDecl.SlotRule("interface3") + c.Assert(slotRule3, NotNil) + c.Assert(slotRule3.DenyInstallation, HasLen, 1) + c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) + c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) + c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + slotRule4 := snapDecl.SlotRule("interface4") + c.Assert(slotRule4, NotNil) + c.Assert(slotRule4.AllowAutoConnection, HasLen, 1) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Assert(slotRule4.DenyAutoConnection, HasLen, 1) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Assert(slotRule4.AllowInstallation, HasLen, 1) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) +} + +func (sds *snapDeclSuite) TestSuggestedFormat(c *C) { + fmtnum, err := asserts.SuggestFormat(asserts.SnapDeclarationType, nil, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 0) + + headers := map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface1": "true", + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 1) + + headers = map[string]interface{}{ + "slots": map[string]interface{}{ + "interface2": "true", + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 1) + + headers = map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "plug-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 2) + + headers = map[string]interface{}{ + "slots": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "plug-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 2) + + // errors + headers = map[string]interface{}{ + "plugs": "what", + } + _, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, ErrorMatches, `assertion snap-declaration: "plugs" header must be a map`) + + headers = map[string]interface{}{ + "slots": "what", + } + _, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, ErrorMatches, `assertion snap-declaration: "slots" header must be a map`) +} + +func prereqDevAccount(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { + dev1Acct := assertstest.NewAccount(storeDB, "developer1", map[string]interface{}{ + "account-id": "dev-id1", + }, "") + err := db.Add(dev1Acct) + c.Assert(err, IsNil) +} + +func (sds *snapDeclSuite) TestSnapDeclarationCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapDecl) + c.Assert(err, IsNil) +} + +func (sds *snapDeclSuite) TestSnapDeclarationCheckUntrustedAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := otherDB.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapDecl) + c.Assert(err, ErrorMatches, `snap-declaration assertion for "foo" \(id "snap-id-1"\) is not signed by a directly trusted authority:.*`) +} + +func (sds *snapDeclSuite) TestSnapDeclarationCheckMissingPublisherAccount(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapDecl) + c.Assert(err, ErrorMatches, `snap-declaration assertion for "foo" \(id "snap-id-1"\) does not have a matching account assertion for the publisher "dev-id1"`) +} + +type snapFileDigestSuite struct{} + +func (s *snapFileDigestSuite) TestSnapFileSHA3_384(c *C) { + exData := []byte("hashmeplease") + + tempdir := c.MkDir() + snapFn := filepath.Join(tempdir, "ex.snap") + err := ioutil.WriteFile(snapFn, exData, 0644) + c.Assert(err, IsNil) + + encDgst, size, err := asserts.SnapFileSHA3_384(snapFn) + c.Assert(err, IsNil) + c.Check(size, Equals, uint64(len(exData))) + + h3_384 := sha3.Sum384(exData) + expected := base64.RawURLEncoding.EncodeToString(h3_384[:]) + c.Check(encDgst, DeepEquals, expected) +} + +type snapBuildSuite struct { + ts time.Time + tsLine string +} + +func (sds *snapDeclSuite) TestPrerequisites(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + sds.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 1) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"dev-id1"}, + }) +} + +func (sbs *snapBuildSuite) SetUpSuite(c *C) { + sbs.ts = time.Now().Truncate(time.Second).UTC() + sbs.tsLine = "timestamp: " + sbs.ts.Format(time.RFC3339) + "\n" +} + +const ( + blobSHA3_384 = "QlqR0uAWEAWF5Nwnzj5kqmmwFslYPu1IL16MKtLKhwhv0kpBv5wKZ_axf_nf_2cL" +) + +func (sbs *snapBuildSuite) TestDecodeOK(c *C) { + encoded := "type: snap-build\n" + + "authority-id: dev-id1\n" + + "snap-sha3-384: " + blobSHA3_384 + "\n" + + "grade: stable\n" + + "snap-id: snap-id-1\n" + + "snap-size: 10000\n" + + sbs.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapBuildType) + snapBuild := a.(*asserts.SnapBuild) + c.Check(snapBuild.AuthorityID(), Equals, "dev-id1") + c.Check(snapBuild.Timestamp(), Equals, sbs.ts) + c.Check(snapBuild.SnapID(), Equals, "snap-id-1") + c.Check(snapBuild.SnapSHA3_384(), Equals, blobSHA3_384) + c.Check(snapBuild.SnapSize(), Equals, uint64(10000)) + c.Check(snapBuild.Grade(), Equals, "stable") +} + +const ( + snapBuildErrPrefix = "assertion snap-build: " +) + +func (sbs *snapBuildSuite) TestDecodeInvalid(c *C) { + digestHdr := "snap-sha3-384: " + blobSHA3_384 + "\n" + + encoded := "type: snap-build\n" + + "authority-id: dev-id1\n" + + digestHdr + + "grade: stable\n" + + "snap-id: snap-id-1\n" + + "snap-size: 10000\n" + + sbs.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {digestHdr, "", `"snap-sha3-384" header is mandatory`}, + {digestHdr, "snap-sha3-384: \n", `"snap-sha3-384" header should not be empty`}, + {digestHdr, "snap-sha3-384: #\n", `"snap-sha3-384" header cannot be decoded:.*`}, + {"snap-size: 10000\n", "", `"snap-size" header is mandatory`}, + {"snap-size: 10000\n", "snap-size: -1\n", `"snap-size" header is not an unsigned integer: -1`}, + {"snap-size: 10000\n", "snap-size: zzz\n", `"snap-size" header is not an unsigned integer: zzz`}, + {"grade: stable\n", "", `"grade" header is mandatory`}, + {"grade: stable\n", "grade: \n", `"grade" header should not be empty`}, + {sbs.tsLine, "", `"timestamp" header is mandatory`}, + {sbs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {sbs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapBuildErrPrefix+test.expectedErr) + } +} + +func makeStoreAndCheckDB(c *C) (store *assertstest.StoreStack, checkDB *asserts.Database) { + store = assertstest.NewStoreStack("canonical", nil) + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + OtherPredefined: store.Generic, + } + checkDB, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + // add store key + err = checkDB.Add(store.StoreAccountKey("")) + c.Assert(err, IsNil) + // add generic key + err = checkDB.Add(store.GenericKey) + c.Assert(err, IsNil) + + return store, checkDB +} + +func setup3rdPartySigning(c *C, username string, storeDB assertstest.SignerDB, checkDB *asserts.Database) (signingDB *assertstest.SigningDB) { + privKey := testPrivKey2 + + acct := assertstest.NewAccount(storeDB, username, map[string]interface{}{ + "account-id": username, + }, "") + accKey := assertstest.NewAccountKey(storeDB, acct, nil, privKey.PublicKey(), "") + + err := checkDB.Add(acct) + c.Assert(err, IsNil) + err = checkDB.Add(accKey) + c.Assert(err, IsNil) + + return assertstest.NewSigningDB(acct.AccountID(), privKey) +} + +func (sbs *snapBuildSuite) TestSnapBuildCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "devel1", storeDB, db) + + headers := map[string]interface{}{ + "authority-id": "devel1", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": time.Now().Format(time.RFC3339), + } + snapBuild, err := devDB.Sign(asserts.SnapBuildType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapBuild) + c.Assert(err, IsNil) +} + +func (sbs *snapBuildSuite) TestSnapBuildCheckInconsistentTimestamp(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "devel1", storeDB, db) + + headers := map[string]interface{}{ + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "grade": "devel", + "snap-size": "1025", + "timestamp": "2013-01-01T14:00:00Z", + } + snapBuild, err := devDB.Sign(asserts.SnapBuildType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapBuild) + c.Assert(err, ErrorMatches, `snap-build assertion timestamp outside of signing key validity \(key valid since.*\)`) +} + +type snapRevSuite struct { + ts time.Time + tsLine string +} + +func (srs *snapRevSuite) SetUpSuite(c *C) { + srs.ts = time.Now().Truncate(time.Second).UTC() + srs.tsLine = "timestamp: " + srs.ts.Format(time.RFC3339) + "\n" +} + +func (srs *snapRevSuite) makeValidEncoded() string { + return "type: snap-revision\n" + + "authority-id: store-id1\n" + + "snap-sha3-384: " + blobSHA3_384 + "\n" + + "snap-id: snap-id-1\n" + + "snap-size: 123\n" + + "snap-revision: 1\n" + + "developer-id: dev-id1\n" + + "revision: 1\n" + + srs.tsLine + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +} + +func (srs *snapRevSuite) makeHeaders(overrides map[string]interface{}) map[string]interface{} { + headers := map[string]interface{}{ + "authority-id": "canonical", + "snap-sha3-384": blobSHA3_384, + "snap-id": "snap-id-1", + "snap-size": "123", + "snap-revision": "1", + "developer-id": "dev-id1", + "revision": "1", + "timestamp": time.Now().Format(time.RFC3339), + } + for k, v := range overrides { + headers[k] = v + } + return headers +} + +func (srs *snapRevSuite) TestDecodeOK(c *C) { + encoded := srs.makeValidEncoded() + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapRevisionType) + snapRev := a.(*asserts.SnapRevision) + c.Check(snapRev.AuthorityID(), Equals, "store-id1") + c.Check(snapRev.Timestamp(), Equals, srs.ts) + c.Check(snapRev.SnapID(), Equals, "snap-id-1") + c.Check(snapRev.SnapSHA3_384(), Equals, blobSHA3_384) + c.Check(snapRev.SnapSize(), Equals, uint64(123)) + c.Check(snapRev.SnapRevision(), Equals, 1) + c.Check(snapRev.DeveloperID(), Equals, "dev-id1") + c.Check(snapRev.Revision(), Equals, 1) +} + +const ( + snapRevErrPrefix = "assertion snap-revision: " +) + +func (srs *snapRevSuite) TestDecodeInvalid(c *C) { + encoded := srs.makeValidEncoded() + + digestHdr := "snap-sha3-384: " + blobSHA3_384 + "\n" + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {digestHdr, "", `"snap-sha3-384" header is mandatory`}, + {digestHdr, "snap-sha3-384: \n", `"snap-sha3-384" header should not be empty`}, + {digestHdr, "snap-sha3-384: #\n", `"snap-sha3-384" header cannot be decoded:.*`}, + {digestHdr, "snap-sha3-384: eHl6\n", `"snap-sha3-384" header does not have the expected bit length: 24`}, + {"snap-size: 123\n", "", `"snap-size" header is mandatory`}, + {"snap-size: 123\n", "snap-size: \n", `"snap-size" header should not be empty`}, + {"snap-size: 123\n", "snap-size: -1\n", `"snap-size" header is not an unsigned integer: -1`}, + {"snap-size: 123\n", "snap-size: zzz\n", `"snap-size" header is not an unsigned integer: zzz`}, + {"snap-revision: 1\n", "", `"snap-revision" header is mandatory`}, + {"snap-revision: 1\n", "snap-revision: \n", `"snap-revision" header should not be empty`}, + {"snap-revision: 1\n", "snap-revision: -1\n", `"snap-revision" header must be >=1: -1`}, + {"snap-revision: 1\n", "snap-revision: 0\n", `"snap-revision" header must be >=1: 0`}, + {"snap-revision: 1\n", "snap-revision: zzz\n", `"snap-revision" header is not an integer: zzz`}, + {"developer-id: dev-id1\n", "", `"developer-id" header is mandatory`}, + {"developer-id: dev-id1\n", "developer-id: \n", `"developer-id" header should not be empty`}, + {srs.tsLine, "", `"timestamp" header is mandatory`}, + {srs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {srs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapRevErrPrefix+test.expectedErr) + } +} + +func prereqSnapDecl(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) +} + +func (srs *snapRevSuite) TestSnapRevisionCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + prereqSnapDecl(c, storeDB, db) + + headers := srs.makeHeaders(nil) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, IsNil) +} + +func (srs *snapRevSuite) TestSnapRevisionCheckInconsistentTimestamp(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + headers := srs.makeHeaders(map[string]interface{}{ + "timestamp": "2013-01-01T14:00:00Z", + }) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, ErrorMatches, `snap-revision assertion timestamp outside of signing key validity \(key valid since.*\)`) +} + +func (srs *snapRevSuite) TestSnapRevisionCheckUntrustedAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := srs.makeHeaders(map[string]interface{}{ + "authority-id": "other", + }) + snapRev, err := otherDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" is not signed by a store:.*`) +} + +func (srs *snapRevSuite) TestSnapRevisionCheckMissingDeveloperAccount(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + headers := srs.makeHeaders(nil) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" does not have a matching account assertion for the developer "dev-id1"`) +} + +func (srs *snapRevSuite) TestSnapRevisionCheckMissingDeclaration(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + + headers := srs.makeHeaders(nil) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" does not have a matching snap-declaration assertion`) +} + +func (srs *snapRevSuite) TestPrimaryKey(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + prereqSnapDecl(c, storeDB, db) + + headers := srs.makeHeaders(nil) + snapRev, err := storeDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapRev) + c.Assert(err, IsNil) + + _, err = db.Find(asserts.SnapRevisionType, map[string]string{ + "snap-sha3-384": headers["snap-sha3-384"].(string), + }) + c.Assert(err, IsNil) +} + +func (srs *snapRevSuite) TestPrerequisites(c *C) { + encoded := srs.makeValidEncoded() + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 2) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{"16", "snap-id-1"}, + }) + c.Check(prereqs[1], DeepEquals, &asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"dev-id1"}, + }) +} + +type validationSuite struct { + ts time.Time + tsLine string +} + +func (vs *validationSuite) SetUpSuite(c *C) { + vs.ts = time.Now().Truncate(time.Second).UTC() + vs.tsLine = "timestamp: " + vs.ts.Format(time.RFC3339) + "\n" +} + +func (vs *validationSuite) makeValidEncoded() string { + return "type: validation\n" + + "authority-id: dev-id1\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "approved-snap-id: snap-id-2\n" + + "approved-snap-revision: 42\n" + + "revision: 1\n" + + vs.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +} + +func (vs *validationSuite) makeHeaders(overrides map[string]interface{}) map[string]interface{} { + headers := map[string]interface{}{ + "authority-id": "dev-id1", + "series": "16", + "snap-id": "snap-id-1", + "approved-snap-id": "snap-id-2", + "approved-snap-revision": "42", + "revision": "1", + "timestamp": time.Now().Format(time.RFC3339), + } + for k, v := range overrides { + headers[k] = v + } + return headers +} + +func (vs *validationSuite) TestDecodeOK(c *C) { + encoded := vs.makeValidEncoded() + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ValidationType) + validation := a.(*asserts.Validation) + c.Check(validation.AuthorityID(), Equals, "dev-id1") + c.Check(validation.Timestamp(), Equals, vs.ts) + c.Check(validation.Series(), Equals, "16") + c.Check(validation.SnapID(), Equals, "snap-id-1") + c.Check(validation.ApprovedSnapID(), Equals, "snap-id-2") + c.Check(validation.ApprovedSnapRevision(), Equals, 42) + c.Check(validation.Revoked(), Equals, false) + c.Check(validation.Revision(), Equals, 1) +} + +const ( + validationErrPrefix = "assertion validation: " +) + +func (vs *validationSuite) TestDecodeInvalid(c *C) { + encoded := vs.makeValidEncoded() + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {"approved-snap-id: snap-id-2\n", "", `"approved-snap-id" header is mandatory`}, + {"approved-snap-id: snap-id-2\n", "approved-snap-id: \n", `"approved-snap-id" header should not be empty`}, + {"approved-snap-revision: 42\n", "", `"approved-snap-revision" header is mandatory`}, + {"approved-snap-revision: 42\n", "approved-snap-revision: z\n", `"approved-snap-revision" header is not an integer: z`}, + {"approved-snap-revision: 42\n", "approved-snap-revision: 0\n", `"approved-snap-revision" header must be >=1: 0`}, + {"approved-snap-revision: 42\n", "approved-snap-revision: -1\n", `"approved-snap-revision" header must be >=1: -1`}, + {vs.tsLine, "", `"timestamp" header is mandatory`}, + {vs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {vs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, validationErrPrefix+test.expectedErr) + } +} + +func prereqSnapDecl2(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-2", + "snap-name": "bar", + "publisher-id": "dev-id1", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) +} + +func (vs *validationSuite) TestValidationCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + prereqSnapDecl(c, storeDB, db) + prereqSnapDecl2(c, storeDB, db) + + headers := vs.makeHeaders(nil) + validation, err := devDB.Sign(asserts.ValidationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(validation) + c.Assert(err, IsNil) +} + +func (vs *validationSuite) TestValidationCheckWrongAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + prereqDevAccount(c, storeDB, db) + prereqSnapDecl(c, storeDB, db) + prereqSnapDecl2(c, storeDB, db) + + headers := vs.makeHeaders(map[string]interface{}{ + "authority-id": "canonical", // not the publisher + }) + validation, err := storeDB.Sign(asserts.ValidationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(validation) + c.Assert(err, ErrorMatches, `validation assertion by snap "foo" \(id "snap-id-1"\) not signed by its publisher`) +} + +func (vs *validationSuite) TestRevocation(c *C) { + encoded := "type: validation\n" + + "authority-id: dev-id1\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "approved-snap-id: snap-id-2\n" + + "approved-snap-revision: 42\n" + + "revoked: true\n" + + "revision: 1\n" + + vs.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + validation := a.(*asserts.Validation) + c.Check(validation.Revoked(), Equals, true) +} + +func (vs *validationSuite) TestRevokedFalse(c *C) { + encoded := "type: validation\n" + + "authority-id: dev-id1\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "approved-snap-id: snap-id-2\n" + + "approved-snap-revision: 42\n" + + "revoked: false\n" + + "revision: 1\n" + + vs.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + validation := a.(*asserts.Validation) + c.Check(validation.Revoked(), Equals, false) +} + +func (vs *validationSuite) TestRevokedInvalid(c *C) { + encoded := "type: validation\n" + + "authority-id: dev-id1\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "approved-snap-id: snap-id-2\n" + + "approved-snap-revision: 42\n" + + "revoked: foo\n" + + "revision: 1\n" + + vs.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, ErrorMatches, `.*: "revoked" header must be 'true' or 'false'`) +} + +func (vs *validationSuite) TestMissingGatedSnapDeclaration(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + headers := vs.makeHeaders(nil) + a, err := devDB.Sign(asserts.ValidationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(a) + c.Assert(err, ErrorMatches, `validation assertion by snap-id "snap-id-1" does not have a matching snap-declaration assertion for approved-snap-id "snap-id-2"`) +} + +func (vs *validationSuite) TestMissingGatingSnapDeclaration(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + prereqSnapDecl2(c, storeDB, db) + + headers := vs.makeHeaders(nil) + a, err := devDB.Sign(asserts.ValidationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(a) + c.Assert(err, ErrorMatches, `validation assertion by snap-id "snap-id-1" does not have a matching snap-declaration assertion`) +} + +func (vs *validationSuite) TestPrerequisites(c *C) { + encoded := vs.makeValidEncoded() + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 2) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{"16", "snap-id-1"}, + }) + c.Check(prereqs[1], DeepEquals, &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{"16", "snap-id-2"}, + }) +} + +type baseDeclSuite struct{} + +func (s *baseDeclSuite) TestDecodeOK(c *C) { + encoded := `type: base-declaration +authority-id: canonical +series: 16 +plugs: + interface1: + deny-installation: false + allow-auto-connection: + slot-snap-type: + - app + slot-publisher-id: + - acme + slot-attributes: + a1: /foo/.* + plug-attributes: + b1: B1 + deny-auto-connection: + slot-attributes: + a1: !A1 + plug-attributes: + b1: !B1 + interface2: + allow-installation: true + allow-connection: + plug-attributes: + a2: A2 + slot-attributes: + b2: B2 + deny-connection: + slot-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + a2: !A2 + slot-attributes: + b2: !B2 +slots: + interface3: + deny-installation: false + allow-auto-connection: + plug-snap-type: + - app + plug-publisher-id: + - acme + slot-attributes: + c1: /foo/.* + plug-attributes: + d1: C1 + deny-auto-connection: + slot-attributes: + c1: !C1 + plug-attributes: + d1: !D1 + interface4: + allow-connection: + plug-attributes: + c2: C2 + slot-attributes: + d2: D2 + deny-connection: + plug-snap-id: + - snapidsnapidsnapidsnapidsnapid01 + - snapidsnapidsnapidsnapidsnapid02 + plug-attributes: + c2: !D2 + slot-attributes: + d2: !D2 + allow-installation: + slot-snap-type: + - app + slot-attributes: + e1: E1 +timestamp: 2016-09-29T19:50:49Z +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw==` + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + baseDecl := a.(*asserts.BaseDeclaration) + c.Check(baseDecl.Series(), Equals, "16") + ts, err := time.Parse(time.RFC3339, "2016-09-29T19:50:49Z") + c.Assert(err, IsNil) + c.Check(baseDecl.Timestamp().Equal(ts), Equals, true) + + c.Check(baseDecl.PlugRule("interfaceX"), IsNil) + c.Check(baseDecl.SlotRule("interfaceX"), IsNil) + + plugRule1 := baseDecl.PlugRule("interface1") + c.Assert(plugRule1, NotNil) + c.Assert(plugRule1.DenyInstallation, HasLen, 1) + c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) + c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) + c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + plugRule2 := baseDecl.PlugRule("interface2") + c.Assert(plugRule2, NotNil) + c.Assert(plugRule2.AllowInstallation, HasLen, 1) + c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Assert(plugRule2.AllowConnection, HasLen, 1) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Assert(plugRule2.DenyConnection, HasLen, 1) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + + slotRule3 := baseDecl.SlotRule("interface3") + c.Assert(slotRule3, NotNil) + c.Assert(slotRule3.DenyInstallation, HasLen, 1) + c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) + c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) + c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) + c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + slotRule4 := baseDecl.SlotRule("interface4") + c.Assert(slotRule4, NotNil) + c.Assert(slotRule4.AllowConnection, HasLen, 1) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Assert(slotRule4.DenyConnection, HasLen, 1) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) + c.Assert(slotRule4.AllowInstallation, HasLen, 1) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) + +} + +func (s *baseDeclSuite) TestBaseDeclarationCheckUntrustedAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + + headers := map[string]interface{}{ + "series": "16", + "timestamp": time.Now().Format(time.RFC3339), + } + baseDecl, err := otherDB.Sign(asserts.BaseDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(baseDecl) + c.Assert(err, ErrorMatches, `base-declaration assertion for series 16 is not signed by a directly trusted authority: other`) +} + +const ( + baseDeclErrPrefix = "assertion base-declaration: " +) + +func (s *baseDeclSuite) TestDecodeInvalid(c *C) { + tsLine := "timestamp: 2016-09-29T19:50:49Z\n" + + encoded := "type: base-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "plugs:\n interface1: true\n" + + "slots:\n interface2: true\n" + + tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"plugs:\n interface1: true\n", "plugs: \n", `"plugs" header must be a map`}, + {"plugs:\n interface1: true\n", "plugs:\n intf1:\n foo: bar\n", `plug rule for interface "intf1" must specify at least one of.*`}, + {"slots:\n interface2: true\n", "slots: \n", `"slots" header must be a map`}, + {"slots:\n interface2: true\n", "slots:\n intf1:\n foo: bar\n", `slot rule for interface "intf1" must specify at least one of.*`}, + {tsLine, "", `"timestamp" header is mandatory`}, + {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, baseDeclErrPrefix+test.expectedErr) + } + +} + +func (s *baseDeclSuite) TestBuiltin(c *C) { + baseDecl := asserts.BuiltinBaseDeclaration() + c.Check(baseDecl, IsNil) + + defer asserts.InitBuiltinBaseDeclaration(nil) + + const headers = ` +type: base-declaration +authority-id: canonical +series: 16 +revision: 0 +plugs: + network: true +slots: + network: + allow-installation: + slot-snap-type: + - core +` + + err := asserts.InitBuiltinBaseDeclaration([]byte(headers)) + c.Assert(err, IsNil) + + baseDecl = asserts.BuiltinBaseDeclaration() + c.Assert(baseDecl, NotNil) + + cont, _ := baseDecl.Signature() + c.Check(string(cont), Equals, strings.TrimSpace(headers)) + + c.Check(baseDecl.AuthorityID(), Equals, "canonical") + c.Check(baseDecl.Series(), Equals, "16") + c.Check(baseDecl.PlugRule("network").AllowAutoConnection[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) + c.Check(baseDecl.SlotRule("network").AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"core"}) + + enc := asserts.Encode(baseDecl) + // it's expected that it cannot be decoded + _, err = asserts.Decode(enc) + c.Check(err, NotNil) +} + +func (s *baseDeclSuite) TestBuiltinInitErrors(c *C) { + defer asserts.InitBuiltinBaseDeclaration(nil) + + tests := []struct { + headers string + err string + }{ + {"", `header entry missing ':' separator: ""`}, + {"type: foo\n", `the builtin base-declaration "type" header is not set to expected value "base-declaration"`}, + {"type: base-declaration", `the builtin base-declaration "authority-id" header is not set to expected value "canonical"`}, + {"type: base-declaration\nauthority-id: canonical", `the builtin base-declaration "series" header is not set to expected value "16"`}, + {"type: base-declaration\nauthority-id: canonical\nseries: 16\nrevision: zzz", `cannot assemble the builtin-base declaration: "revision" header is not an integer: zzz`}, + {"type: base-declaration\nauthority-id: canonical\nseries: 16\nplugs: foo", `cannot assemble the builtin base-declaration: "plugs" header must be a map`}, + } + + for _, t := range tests { + err := asserts.InitBuiltinBaseDeclaration([]byte(t.headers)) + c.Check(err, ErrorMatches, t.err, Commentf(t.headers)) + } +} + +type snapDevSuite struct { + developersLines string + validEncoded string +} + +func (sds *snapDevSuite) SetUpSuite(c *C) { + sds.developersLines = "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-02-01T00:00:00.0Z\n" + sds.validEncoded = "type: snap-developer\n" + + "authority-id: dev-id1\n" + + "snap-id: snap-id-1\n" + + "publisher-id: dev-id1\n" + + sds.developersLines + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +} + +func (sds *snapDevSuite) TestDecodeOK(c *C) { + encoded := sds.validEncoded + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapDeveloperType) + snapDev := a.(*asserts.SnapDeveloper) + c.Check(snapDev.AuthorityID(), Equals, "dev-id1") + c.Check(snapDev.PublisherID(), Equals, "dev-id1") + c.Check(snapDev.SnapID(), Equals, "snap-id-1") +} + +func (sds *snapDevSuite) TestDevelopersOptional(c *C) { + encoded := strings.Replace(sds.validEncoded, sds.developersLines, "", 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) +} + +func (sds *snapDevSuite) TestDevelopersUntilOptional(c *C) { + encoded := strings.Replace( + sds.validEncoded, sds.developersLines, + "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n", 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) +} + +func (sds *snapDevSuite) TestDevelopersRevoked(c *C) { + encoded := sds.validEncoded + encoded = strings.Replace( + encoded, sds.developersLines, + "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n", 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) + // TODO(matt): check actually revoked rather than just parsed +} + +const ( + snapDevErrPrefix = "assertion snap-developer: " +) + +func (sds *snapDevSuite) TestDecodeInvalid(c *C) { + encoded := sds.validEncoded + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"publisher-id: dev-id1\n", "", `"publisher-id" header is mandatory`}, + {"publisher-id: dev-id1\n", "publisher-id: \n", `"publisher-id" header should not be empty`}, + {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`}, + {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`}, + {sds.developersLines, "developers: \n", `"developers" must be a list of developer maps`}, + {sds.developersLines, "developers: foo\n", `"developers" must be a list of developer maps`}, + {sds.developersLines, "developers:\n foo: bar\n", `"developers" must be a list of developer maps`}, + {sds.developersLines, "developers:\n - foo\n", `"developers" must be a list of developer maps`}, + {sds.developersLines, "developers:\n -\n foo: bar\n", `"developer-id" in "developers" item 1 is mandatory`}, + {sds.developersLines, "developers:\n -\n developer-id: a\n", + `"developer-id" in "developers" item 1 contains invalid characters: "a"`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n", + `"since" in "developers" item 1 for developer "dev-id2" is mandatory`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: \n", + `"since" in "developers" item 1 for developer "dev-id2" should not be empty`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: foo\n", + `"since" in "developers" item 1 for developer "dev-id2" is not a RFC3339 date.*`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: \n", + `"until" in "developers" item 1 for developer "dev-id2" is not a RFC3339 date.*`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: foo\n", + `"until" in "developers" item 1 for developer "dev-id2" is not a RFC3339 date.*`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n -\n foo: bar\n", + `"developer-id" in "developers" item 2 is mandatory`}, + {sds.developersLines, "developers:\n -\n developer-id: dev-id2\n since: 2017-01-02T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n", + `"since" in "developers" item 1 for developer "dev-id2" must be less than or equal to "until"`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapDevErrPrefix+test.expectedErr) + } +} + +func (sds *snapDevSuite) TestRevokedValidation(c *C) { + // Multiple non-revoking items are fine. + encoded := strings.Replace(sds.validEncoded, sds.developersLines, + "developers:\n"+ + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-02-01T00:00:00.0Z\n"+ + " -\n developer-id: dev-id2\n since: 2017-03-01T00:00:00.0Z\n", + 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) + + // Multiple revocations for different developers are fine. + encoded = strings.Replace(sds.validEncoded, sds.developersLines, + "developers:\n"+ + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n"+ + " -\n developer-id: dev-id3\n since: 2017-02-01T00:00:00.0Z\n until: 2017-02-01T00:00:00.0Z\n", + 1) + _, err = asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) + + invalidTests := []string{ + // Multiple revocations. + "developers:\n" + + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n" + + " -\n developer-id: dev-id2\n since: 2017-02-01T00:00:00.0Z\n until: 2017-02-01T00:00:00.0Z\n", + // Revocation after non-revoking. + "developers:\n" + + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n" + + " -\n developer-id: dev-id2\n since: 2017-03-01T00:00:00.0Z\n until: 2017-03-01T00:00:00.0Z\n", + // Non-revoking after revocation. + "developers:\n" + + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n until: 2017-01-01T00:00:00.0Z\n" + + " -\n developer-id: dev-id2\n since: 2017-02-01T00:00:00.0Z\n", + } + for _, test := range invalidTests { + encoded := strings.Replace(sds.validEncoded, sds.developersLines, test, 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, ErrorMatches, snapDevErrPrefix+`revocation for developer "dev-id2" must be standalone but found other "developers" items`) + } +} + +func (sds *snapDevSuite) TestAuthorityIsPublisher(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := devDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "snap-id": "snap-id-1", + "publisher-id": "dev-id1", + }, nil, "") + c.Assert(err, IsNil) + // Just to be super sure ... + c.Assert(snapDev.HeaderString("authority-id"), Equals, "dev-id1") + c.Assert(snapDev.HeaderString("publisher-id"), Equals, "dev-id1") + + err = db.Check(snapDev) + c.Assert(err, IsNil) +} + +func (sds *snapDevSuite) TestAuthorityIsNotPublisher(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := devDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "authority-id": "dev-id1", + "snap-id": "snap-id-1", + "publisher-id": "dev-id2", + }, nil, "") + c.Assert(err, IsNil) + // Just to be super sure ... + c.Assert(snapDev.HeaderString("authority-id"), Equals, "dev-id1") + c.Assert(snapDev.HeaderString("publisher-id"), Equals, "dev-id2") + + err = db.Check(snapDev) + c.Assert(err, ErrorMatches, `snap-developer must be signed by the publisher or a trusted authority but got authority "dev-id1" and publisher "dev-id2"`) +} + +func (sds *snapDevSuite) TestAuthorityIsNotPublisherButIsTrusted(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + account, err := storeDB.Sign(asserts.AccountType, map[string]interface{}{ + "account-id": "dev-id1", + "display-name": "dev-id1", + "validation": "unknown", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(account) + c.Assert(err, IsNil) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := storeDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "snap-id": "snap-id-1", + "publisher-id": "dev-id1", + }, nil, "") + c.Assert(err, IsNil) + // Just to be super sure ... + c.Assert(snapDev.HeaderString("authority-id"), Equals, "canonical") + c.Assert(snapDev.HeaderString("publisher-id"), Equals, "dev-id1") + + err = db.Check(snapDev) + c.Assert(err, IsNil) +} + +func (sds *snapDevSuite) TestCheckNewPublisherAccountExists(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + account, err := storeDB.Sign(asserts.AccountType, map[string]interface{}{ + "account-id": "dev-id1", + "display-name": "dev-id1", + "validation": "unknown", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(account) + c.Assert(err, IsNil) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := storeDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "snap-id": "snap-id-1", + "publisher-id": "dev-id2", + }, nil, "") + c.Assert(err, IsNil) + // Just to be super sure ... + c.Assert(snapDev.HeaderString("authority-id"), Equals, "canonical") + c.Assert(snapDev.HeaderString("publisher-id"), Equals, "dev-id2") + + // There's no account for dev-id2 yet so it should fail. + err = db.Check(snapDev) + c.Assert(err, ErrorMatches, `snap-developer assertion for snap-id "snap-id-1" does not have a matching account assertion for the publisher "dev-id2"`) + + // But once the dev-id2 account is added the snap-developer is ok. + account, err = storeDB.Sign(asserts.AccountType, map[string]interface{}{ + "account-id": "dev-id2", + "display-name": "dev-id2", + "validation": "unknown", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(account) + c.Assert(err, IsNil) + + err = db.Check(snapDev) + c.Assert(err, IsNil) +} + +func (sds *snapDevSuite) TestCheckDeveloperAccountExists(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "snap-name-1", + "publisher-id": "dev-id1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + snapDev, err := devDB.Sign(asserts.SnapDeveloperType, map[string]interface{}{ + "snap-id": "snap-id-1", + "publisher-id": "dev-id1", + "developers": []interface{}{ + map[string]interface{}{ + "developer-id": "dev-id2", + "since": "2017-01-01T00:00:00.0Z", + }, + }, + }, nil, "") + c.Assert(err, IsNil) + err = db.Check(snapDev) + c.Assert(err, ErrorMatches, `snap-developer assertion for snap-id "snap-id-1" does not have a matching account assertion for the developer "dev-id2"`) +} + +func (sds *snapDevSuite) TestCheckMissingDeclaration(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db) + + headers := map[string]interface{}{ + "authority-id": "dev-id1", + "snap-id": "snap-id-1", + "publisher-id": "dev-id1", + } + snapDev, err := devDB.Sign(asserts.SnapDeveloperType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapDev) + c.Assert(err, ErrorMatches, `snap-developer assertion for snap id "snap-id-1" does not have a matching snap-declaration assertion`) +} + +func (sds *snapDevSuite) TestPrerequisitesNoDevelopers(c *C) { + encoded := strings.Replace(sds.validEncoded, sds.developersLines, "", 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + prereqs := assert.Prerequisites() + sort.Sort(RefSlice(prereqs)) + c.Assert(prereqs, DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id1"}}, + {Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-1"}}, + }) +} + +func (sds *snapDevSuite) TestPrerequisitesWithDevelopers(c *C) { + encoded := strings.Replace( + sds.validEncoded, sds.developersLines, + "developers:\n"+ + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n"+ + " -\n developer-id: dev-id3\n since: 2017-01-01T00:00:00.0Z\n", + 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + prereqs := assert.Prerequisites() + sort.Sort(RefSlice(prereqs)) + c.Assert(prereqs, DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id1"}}, + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id2"}}, + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id3"}}, + {Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-1"}}, + }) +} + +func (sds *snapDevSuite) TestPrerequisitesWithDeveloperRepeated(c *C) { + encoded := strings.Replace( + sds.validEncoded, sds.developersLines, + "developers:\n"+ + " -\n developer-id: dev-id2\n since: 2015-01-01T00:00:00.0Z\n until: 2016-01-01T00:00:00.0Z\n"+ + " -\n developer-id: dev-id2\n since: 2017-01-01T00:00:00.0Z\n", + 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + prereqs := assert.Prerequisites() + sort.Sort(RefSlice(prereqs)) + c.Assert(prereqs, DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id1"}}, + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id2"}}, + {Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-1"}}, + }) +} + +func (sds *snapDevSuite) TestPrerequisitesWithPublisherAsDeveloper(c *C) { + encoded := strings.Replace( + sds.validEncoded, sds.developersLines, + "developers:\n -\n developer-id: dev-id1\n since: 2017-01-01T00:00:00.0Z\n", + 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + prereqs := assert.Prerequisites() + sort.Sort(RefSlice(prereqs)) + c.Assert(prereqs, DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"dev-id1"}}, + {Type: asserts.SnapDeclarationType, PrimaryKey: []string{"16", "snap-id-1"}}, + }) +} + +type RefSlice []*asserts.Ref + +func (s RefSlice) Len() int { + return len(s) +} + +func (s RefSlice) Less(i, j int) bool { + iref, jref := s[i], s[j] + if v := strings.Compare(iref.Type.Name, jref.Type.Name); v != 0 { + return v == -1 + } + for n, ipk := range iref.PrimaryKey { + jpk := jref.PrimaryKey[n] + if v := strings.Compare(ipk, jpk); v != 0 { + return v == -1 + } + } + return false +} + +func (s RefSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/asserts/snapasserts/snapasserts.go b/asserts/snapasserts/snapasserts.go new file mode 100644 index 00000000..4e5788ca --- /dev/null +++ b/asserts/snapasserts/snapasserts.go @@ -0,0 +1,145 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +// Package snapasserts offers helpers to handle snap assertions and their checking for installation. +package snapasserts + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" +) + +type Finder interface { + // Find an assertion based on arbitrary headers. Provided + // headers must contain the primary key for the assertion + // type. It returns a asserts.NotFoundError if the assertion + // cannot be found. + Find(assertionType *asserts.AssertionType, headers map[string]string) (asserts.Assertion, error) +} + +func findSnapDeclaration(snapID, name string, db Finder) (*asserts.SnapDeclaration, error) { + a, err := db.Find(asserts.SnapDeclarationType, map[string]string{ + "series": release.Series, + "snap-id": snapID, + }) + if err != nil { + return nil, fmt.Errorf("internal error: cannot find snap declaration for %q: %s", name, snapID) + } + snapDecl := a.(*asserts.SnapDeclaration) + + if snapDecl.SnapName() == "" { + return nil, fmt.Errorf("cannot install snap %q with a revoked snap declaration", name) + } + + return snapDecl, nil +} + +// CrossCheck tries to cross check the name, hash digest and size of a snap plus its metadata in a SideInfo with the relevant snap assertions in a database that should have been populated with them. +func CrossCheck(name, snapSHA3_384 string, snapSize uint64, si *snap.SideInfo, db Finder) error { + // get relevant assertions and do cross checks + a, err := db.Find(asserts.SnapRevisionType, map[string]string{ + "snap-sha3-384": snapSHA3_384, + }) + if err != nil { + return fmt.Errorf("internal error: cannot find pre-populated snap-revision assertion for %q: %s", name, snapSHA3_384) + } + snapRev := a.(*asserts.SnapRevision) + + if snapRev.SnapSize() != snapSize { + return fmt.Errorf("snap %q file does not have expected size according to signatures (download is broken or tampered): %d != %d", name, snapSize, snapRev.SnapSize()) + } + + snapID := si.SnapID + + if snapRev.SnapID() != snapID || snapRev.SnapRevision() != si.Revision.N { + return fmt.Errorf("snap %q does not have expected ID or revision according to assertions (metadata is broken or tampered): %s / %s != %d / %s", name, si.Revision, snapID, snapRev.SnapRevision(), snapRev.SnapID()) + } + + snapDecl, err := findSnapDeclaration(snapID, name, db) + if err != nil { + return err + } + + if snapDecl.SnapName() != name { + return fmt.Errorf("cannot install snap %q that is undergoing a rename to %q", name, snapDecl.SnapName()) + } + + return nil +} + +// DeriveSideInfo tries to construct a SideInfo for the given snap using its digest to find the relevant snap assertions with the information in the given database. It will fail with an asserts.NotFoundError if it cannot find them. +func DeriveSideInfo(snapPath string, db Finder) (*snap.SideInfo, error) { + snapSHA3_384, snapSize, err := asserts.SnapFileSHA3_384(snapPath) + if err != nil { + return nil, err + } + + // get relevant assertions and reconstruct metadata + a, err := db.Find(asserts.SnapRevisionType, map[string]string{ + "snap-sha3-384": snapSHA3_384, + }) + if err != nil { + return nil, err + } + + snapRev := a.(*asserts.SnapRevision) + + if snapRev.SnapSize() != snapSize { + return nil, fmt.Errorf("snap %q does not have expected size according to signatures (broken or tampered): %d != %d", snapPath, snapSize, snapRev.SnapSize()) + } + + snapID := snapRev.SnapID() + + snapDecl, err := findSnapDeclaration(snapID, snapPath, db) + if err != nil { + return nil, err + } + + name := snapDecl.SnapName() + + return &snap.SideInfo{ + RealName: name, + SnapID: snapID, + Revision: snap.R(snapRev.SnapRevision()), + }, nil +} + +// FetchSnapAssertions fetches the assertions matching the snap file digest using the given fetcher. +func FetchSnapAssertions(f asserts.Fetcher, snapSHA3_384 string) error { + // for now starting from the snap-revision will get us all other relevant assertions + ref := &asserts.Ref{ + Type: asserts.SnapRevisionType, + PrimaryKey: []string{snapSHA3_384}, + } + + return f.Fetch(ref) +} + +// FetchSnapDeclaration fetches the snap declaration and its prerequisites for the given snap id using the given fetcher. +func FetchSnapDeclaration(f asserts.Fetcher, snapID string) error { + ref := &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{release.Series, snapID}, + } + + return f.Fetch(ref) +} diff --git a/asserts/snapasserts/snapasserts_test.go b/asserts/snapasserts/snapasserts_test.go new file mode 100644 index 00000000..622e1f50 --- /dev/null +++ b/asserts/snapasserts/snapasserts_test.go @@ -0,0 +1,315 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package snapasserts_test + +import ( + "crypto" + "fmt" + "io/ioutil" + "path/filepath" + "testing" + "time" + + "golang.org/x/crypto/sha3" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/snapasserts" + "github.com/snapcore/snapd/snap" +) + +func TestSnapasserts(t *testing.T) { TestingT(t) } + +type snapassertsSuite struct { + storeSigning *assertstest.StoreStack + dev1Acct *asserts.Account + + localDB *asserts.Database +} + +var _ = Suite(&snapassertsSuite{}) + +func (s *snapassertsSuite) SetUpTest(c *C) { + s.storeSigning = assertstest.NewStoreStack("can0nical", nil) + + s.dev1Acct = assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + + localDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.storeSigning.Trusted, + }) + c.Assert(err, IsNil) + + s.localDB = localDB + + // add in prereqs assertions + err = s.localDB.Add(s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + err = s.localDB.Add(s.dev1Acct) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) +} + +func fakeSnap(rev int) []byte { + fake := fmt.Sprintf("hsqs________________%d", rev) + return []byte(fake) +} + +func fakeHash(rev int) []byte { + h := sha3.Sum384(fakeSnap(rev)) + return h[:] +} + +func makeDigest(rev int) string { + d, err := asserts.EncodeDigest(crypto.SHA3_384, fakeHash(rev)) + if err != nil { + panic(err) + } + return string(d) +} + +func (s *snapassertsSuite) TestCrossCheckHappy(c *C) { + digest := makeDigest(12) + size := uint64(len(fakeSnap(12))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "12", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + // everything cross checks + err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) + c.Check(err, IsNil) +} + +func (s *snapassertsSuite) TestCrossCheckErrors(c *C) { + digest := makeDigest(12) + size := uint64(len(fakeSnap(12))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "12", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + // different size + err = snapasserts.CrossCheck("foo", digest, size+1, si, s.localDB) + c.Check(err, ErrorMatches, fmt.Sprintf(`snap "foo" file does not have expected size according to signatures \(download is broken or tampered\): %d != %d`, size+1, size)) + + // mismatched revision vs what we got from store original info + err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(21), + }, s.localDB) + c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 21 / snap-id-1 != 12 / snap-id-1`) + + // mismatched snap id vs what we got from store original info + err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{ + SnapID: "snap-id-other", + Revision: snap.R(12), + }, s.localDB) + c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 12 / snap-id-other != 12 / snap-id-1`) + + // changed name + err = snapasserts.CrossCheck("baz", digest, size, si, s.localDB) + c.Check(err, ErrorMatches, `cannot install snap "baz" that is undergoing a rename to "foo"`) + +} + +func (s *snapassertsSuite) TestCrossCheckRevokedSnapDecl(c *C) { + // revoked snap declaration (snap-name=="") ! + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + digest := makeDigest(12) + size := uint64(len(fakeSnap(12))) + headers = map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "12", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) + c.Check(err, ErrorMatches, `cannot install snap "foo" with a revoked snap declaration`) +} + +func (s *snapassertsSuite) TestDeriveSideInfoHappy(c *C) { + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + tempdir := c.MkDir() + snapPath := filepath.Join(tempdir, "anon.snap") + err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) + c.Assert(err, IsNil) + + si, err := snapasserts.DeriveSideInfo(snapPath, s.localDB) + c.Assert(err, IsNil) + c.Check(si, DeepEquals, &snap.SideInfo{ + RealName: "foo", + SnapID: "snap-id-1", + Revision: snap.R(42), + Channel: "", + }) +} + +func (s *snapassertsSuite) TestDeriveSideInfoNoSignatures(c *C) { + tempdir := c.MkDir() + snapPath := filepath.Join(tempdir, "anon.snap") + err := ioutil.WriteFile(snapPath, fakeSnap(42), 0644) + c.Assert(err, IsNil) + + _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + // cannot find signatures with metadata for snap + c.Assert(asserts.IsNotFound(err), Equals, true) +} + +func (s *snapassertsSuite) TestDeriveSideInfoSizeMismatch(c *C) { + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size+5), // broken + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + tempdir := c.MkDir() + snapPath := filepath.Join(tempdir, "anon.snap") + err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) + c.Assert(err, IsNil) + + _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + c.Check(err, ErrorMatches, fmt.Sprintf(`snap %q does not have expected size according to signatures \(broken or tampered\): %d != %d`, snapPath, size, size+5)) +} + +func (s *snapassertsSuite) TestDeriveSideInfoRevokedSnapDecl(c *C) { + // revoked snap declaration (snap-name=="") ! + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers = map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + tempdir := c.MkDir() + snapPath := filepath.Join(tempdir, "anon.snap") + err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) + c.Assert(err, IsNil) + + _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + c.Check(err, ErrorMatches, fmt.Sprintf(`cannot install snap %q with a revoked snap declaration`, snapPath)) +} diff --git a/asserts/store_asserts.go b/asserts/store_asserts.go new file mode 100644 index 00000000..b3cddaa2 --- /dev/null +++ b/asserts/store_asserts.go @@ -0,0 +1,147 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "net/url" + "time" +) + +// Store holds a store assertion, defining the configuration needed to connect +// a device to the store. +type Store struct { + assertionBase + url *url.URL + timestamp time.Time +} + +// Store returns the identifying name of the operator's store. +func (store *Store) Store() string { + return store.HeaderString("store") +} + +// OperatorID returns the account id of the store's operator. +func (store *Store) OperatorID() string { + return store.HeaderString("operator-id") +} + +// URL returns the URL of the store's API. +func (store *Store) URL() *url.URL { + return store.url +} + +// Location returns a summary of the store's location/purpose. +func (store *Store) Location() string { + return store.HeaderString("location") +} + +// Timestamp returns the time when the store assertion was issued. +func (store *Store) Timestamp() time.Time { + return store.timestamp +} + +func (store *Store) checkConsistency(db RODatabase, acck *AccountKey) error { + // Will be applied to a system's snapd so must be signed by a trusted authority. + if !db.IsTrustedAccount(store.AuthorityID()) { + return fmt.Errorf("store assertion %q is not signed by a directly trusted authority: %s", + store.Store(), store.AuthorityID()) + } + + _, err := db.Find(AccountType, map[string]string{"account-id": store.OperatorID()}) + if err != nil { + if IsNotFound(err) { + return fmt.Errorf( + "store assertion %q does not have a matching account assertion for the operator %q", + store.Store(), store.OperatorID()) + } + return err + } + + return nil +} + +// Prerequisites returns references to this store's prerequisite assertions. +func (store *Store) Prerequisites() []*Ref { + return []*Ref{ + {AccountType, []string{store.OperatorID()}}, + } +} + +// checkStoreURL validates the "url" header and returns a full URL or nil. +func checkStoreURL(headers map[string]interface{}) (*url.URL, error) { + s, err := checkOptionalString(headers, "url") + if err != nil { + return nil, err + } + + if s == "" { + return nil, nil + } + + errWhat := `"url" header` + + u, err := url.Parse(s) + if err != nil { + return nil, fmt.Errorf("%s must be a valid URL: %s", errWhat, s) + } + if u.Scheme != "http" && u.Scheme != "https" { + return nil, fmt.Errorf(`%s scheme must be "https" or "http": %s`, errWhat, s) + } + if u.Host == "" { + return nil, fmt.Errorf(`%s must have a host: %s`, errWhat, s) + } + if u.RawQuery != "" { + return nil, fmt.Errorf(`%s must not have a query: %s`, errWhat, s) + } + if u.Fragment != "" { + return nil, fmt.Errorf(`%s must not have a fragment: %s`, errWhat, s) + } + + return u, nil +} + +func assembleStore(assert assertionBase) (Assertion, error) { + _, err := checkNotEmptyString(assert.headers, "operator-id") + if err != nil { + return nil, err + } + + url, err := checkStoreURL(assert.headers) + if err != nil { + return nil, err + } + + _, err = checkOptionalString(assert.headers, "location") + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &Store{ + assertionBase: assert, + url: url, + timestamp: timestamp, + }, nil +} diff --git a/asserts/store_asserts_test.go b/asserts/store_asserts_test.go new file mode 100644 index 00000000..0a381b30 --- /dev/null +++ b/asserts/store_asserts_test.go @@ -0,0 +1,219 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "strings" + "time" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + . "gopkg.in/check.v1" +) + +var _ = Suite(&storeSuite{}) + +type storeSuite struct { + ts time.Time + tsLine string + validExample string +} + +func (s *storeSuite) SetUpSuite(c *C) { + s.ts = time.Now().Truncate(time.Second).UTC() + s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n" + s.validExample = "type: store\n" + + "authority-id: canonical\n" + + "store: store1\n" + + "operator-id: op-id1\n" + + "url: https://store.example.com\n" + + "location: upstairs\n" + + s.tsLine + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n" + + "\n" + + "AXNpZw==" +} + +func (s *storeSuite) TestDecodeOK(c *C) { + a, err := asserts.Decode([]byte(s.validExample)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.StoreType) + store := a.(*asserts.Store) + + c.Check(store.OperatorID(), Equals, "op-id1") + c.Check(store.Store(), Equals, "store1") + c.Check(store.URL().String(), Equals, "https://store.example.com") + c.Check(store.Location(), Equals, "upstairs") + c.Check(store.Timestamp().Equal(s.ts), Equals, true) +} + +var storeErrPrefix = "assertion store: " + +func (s *storeSuite) TestDecodeInvalidHeaders(c *C) { + tests := []struct{ original, invalid, expectedErr string }{ + {"store: store1\n", "", `"store" header is mandatory`}, + {"store: store1\n", "store: \n", `"store" header should not be empty`}, + {"operator-id: op-id1\n", "", `"operator-id" header is mandatory`}, + {"operator-id: op-id1\n", "operator-id: \n", `"operator-id" header should not be empty`}, + {"url: https://store.example.com\n", "url:\n - foo\n", `"url" header must be a string`}, + {"location: upstairs\n", "location:\n - foo\n", `"location" header must be a string`}, + {s.tsLine, "", `"timestamp" header is mandatory`}, + {s.tsLine, "timestamp: \n", `"timestamp" header should not be empty`}, + {s.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + } + + for _, test := range tests { + invalid := strings.Replace(s.validExample, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, storeErrPrefix+test.expectedErr) + } +} + +func (s *storeSuite) TestURLOptional(c *C) { + tests := []string{"", "url: \n"} + for _, test := range tests { + encoded := strings.Replace(s.validExample, "url: https://store.example.com\n", test, 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + store := assert.(*asserts.Store) + c.Check(store.URL(), IsNil) + } +} + +func (s *storeSuite) TestURL(c *C) { + tests := []struct { + url string + err string + }{ + // Valid URLs. + {"http://example.com/", ""}, + {"https://example.com/", ""}, + {"https://example.com/some/path/", ""}, + {"https://example.com:443/", ""}, + {"https://example.com:1234/", ""}, + {"https://user:pass@example.com/", ""}, + {"https://token@example.com/", ""}, + + // Invalid URLs. + {"://example.com", `"url" header must be a valid URL`}, + {"example.com", `"url" header scheme must be "https" or "http"`}, + {"//example.com", `"url" header scheme must be "https" or "http"`}, + {"ftp://example.com", `"url" header scheme must be "https" or "http"`}, + {"mailto:someone@example.com", `"url" header scheme must be "https" or "http"`}, + {"https://", `"url" header must have a host`}, + {"https:///", `"url" header must have a host`}, + {"https:///some/path", `"url" header must have a host`}, + {"https://example.com/?foo=bar", `"url" header must not have a query`}, + {"https://example.com/#fragment", `"url" header must not have a fragment`}, + } + + for _, test := range tests { + encoded := strings.Replace( + s.validExample, "url: https://store.example.com\n", + fmt.Sprintf("url: %s\n", test.url), 1) + assert, err := asserts.Decode([]byte(encoded)) + if test.err != "" { + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, storeErrPrefix+test.err+": "+test.url) + } else { + c.Assert(err, IsNil) + c.Check(assert.(*asserts.Store).URL().String(), Equals, test.url) + } + } +} + +func (s *storeSuite) TestLocationOptional(c *C) { + encoded := strings.Replace(s.validExample, "location: upstairs\n", "", 1) + _, err := asserts.Decode([]byte(encoded)) + c.Check(err, IsNil) +} + +func (s *storeSuite) TestLocation(c *C) { + for _, test := range []string{"foo", "bar", ""} { + encoded := strings.Replace( + s.validExample, "location: upstairs\n", + fmt.Sprintf("location: %s\n", test), 1) + assert, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + store := assert.(*asserts.Store) + c.Check(store.Location(), Equals, test) + } +} + +func (s *storeSuite) TestCheckAuthority(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + // Add account for operator. + operator := assertstest.NewAccount(storeDB, "op-id1", nil, "") + err := db.Add(operator) + c.Assert(err, IsNil) + + storeHeaders := map[string]interface{}{ + "store": "store1", + "operator-id": operator.HeaderString("account-id"), + "timestamp": time.Now().Format(time.RFC3339), + } + + // store signed by some other account fails. + otherDB := setup3rdPartySigning(c, "other", storeDB, db) + store, err := otherDB.Sign(asserts.StoreType, storeHeaders, nil, "") + c.Assert(err, IsNil) + err = db.Check(store) + c.Assert(err, ErrorMatches, `store assertion "store1" is not signed by a directly trusted authority: other`) + + // but succeeds when signed by a trusted authority. + store, err = storeDB.Sign(asserts.StoreType, storeHeaders, nil, "") + c.Assert(err, IsNil) + err = db.Check(store) + c.Assert(err, IsNil) +} + +func (s *storeSuite) TestCheckOperatorAccount(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + store, err := storeDB.Sign(asserts.StoreType, map[string]interface{}{ + "store": "store1", + "operator-id": "op-id1", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + // No account for operator op-id1 yet, so Check fails. + err = db.Check(store) + c.Assert(err, ErrorMatches, `store assertion "store1" does not have a matching account assertion for the operator "op-id1"`) + + // Add the op-id1 account. + operator := assertstest.NewAccount(storeDB, "op-id1", map[string]interface{}{"account-id": "op-id1"}, "") + err = db.Add(operator) + c.Assert(err, IsNil) + + // Now the operator exists so Check succeeds. + err = db.Check(store) + c.Assert(err, IsNil) +} + +func (s *storeSuite) TestPrerequisites(c *C) { + assert, err := asserts.Decode([]byte(s.validExample)) + c.Assert(err, IsNil) + c.Assert(assert.Prerequisites(), DeepEquals, []*asserts.Ref{ + {Type: asserts.AccountType, PrimaryKey: []string{"op-id1"}}, + }) +} diff --git a/asserts/sysdb/generic.go b/asserts/sysdb/generic.go new file mode 100644 index 00000000..e12cba59 --- /dev/null +++ b/asserts/sysdb/generic.go @@ -0,0 +1,196 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package sysdb + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" +) + +const ( + encodedGenericAccount = `type: account +authority-id: canonical +account-id: generic +display-name: Generic +timestamp: 2017-07-27T00:00:00.0Z +username: generic +validation: certified +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcLDXAQAAQoABgUCWYuVIgAKCRDUpVvql9g3II66IACcoxSoX8+PQLa9TNuNBUs3bdTW6V5ZOdE8 +vnziIg+yqu3qYfWHcRf1qu7K9Igv5lH3uM5jh2AHlndaoX4Qg1Rm9rOZCkRr1dDUmdRDBXN2pdTA +oydd0Ivpeai4ATbSZs11h50/vN/mxBwM6TzdGHqRNt6lvygAPe7VtfchSW/J0NsSIHr9SUeuIHkJ +C79DV27B+9/m8pnpKJo/Fv8nKGs4sMduKVjrj9Po3UhpZEQWf3I3SeDI5IE4TgoDe+O7neGUtT6W +D9wnMWLphC+rHbJguxXG/fmnUYiM2U8o4WVrs/fjF0zDRH7rY3tbLPbFXf2OD4qfOvS//VLQWeCK +KAgKhwz0d5CqaHyKSplywSvwO/dxlrqOjt39k3EjYxVuNS5UQk/BzPoDZD5maisCFm9JZwqBlWHP +6XTj8rhHSkNAPXezs2ZpVSsdtNYmpLLzWIFsAviuoMjYYDyL6jZrD4RBNrNOvSNQGLezB+eyI5DW +9vr2ppCw8zr49epPvJ4uqj/AILgr52zworl7v/27X67BOSoRMmE4AOnvjSJ8cN6Yt83AuEI4aZbP +DlF2Znqp8o/srtmJ3ZMpsjIsAqVhCeTU6eWXbYfNUlIMSmC6CDwQQzsukU4M6NEwUQbWddiM3iNL +FdeFsBscXg4Qm/0Y3PULriDoct+VpBUhzwVXG+Lj6rjtcX7n1C/7u9i/+WIBJ7jU4FBjwOdgpSCQ +DSCb0PgTM2PfbScFpn3KVYs0kT/Jc40Lpw6CUG9iUIdz5qlJzhbRiuhU8yjEg9q/5lWizAuxcP+P +anNhmNXsme46IJh7WnlzPAVMsToz8bWY01LC3t33pPGlRJo109PMbNK7reMIb4KFiL4Hy7gVmTj9 +uydReVBUTZuMLRq1ShAJNScZ+HTpWruLoiC87Rf1++1KakahmtWYCdlJv/JSOyjSh8D9h0GEmqON +lKmzrNgQS8QhLh5uBcITN2Kt1UFGu2o9I8l0TgD5Uh9fG/R/A536fpcvIzOA/lhVttO6P9POwUVv +RIBZ3TpVOSzQ+ADpDexRUouPLPkgUwVBSctcgafMaj/w8DsULvlOYd3Sqq9a+zg6bZr9oPBKutUn +YkIUWmLW1OsdQ2eBS9dFqzJTAOELxNOUq37UGnIrMbk3Vn8hLK+S/+W9XL6WVxzmL1PT9FJZZ41p +KdaFV+mvrTfyoxuzXxkWbMwQkc56Ifn+IojbDwMI4FcTcl4dOeUrlnqwBJmTTwEhLVkYDvzYsVV9 +4joFUWhp10JMm3lO+3596m0kYWMhyvGfYnH7QcQ3GtMAz82yRHc1X+seeWyD/aIjlHYNYfaJ5Ogs +VC76lXi7swMtA9jV5FJIGmQufLo9f93NSYxqwpa8 +` + + encodedGenericModelsAccountKey = `type: account-key +authority-id: canonical +public-key-sha3-384: d-JcZF9nD9eBw7bwMnH61x-bklnQOhQud1Is6o_cn2wTj8EYDi9musrIT9z2MdAa +account-id: generic +name: models +since: 2017-07-27T00:00:00.0Z +body-length: 717 +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcbBTQRWhcGAARAAoRakbLAMMoPeMz5MLCzAR6ALu/xxP9PuCdkknHH5lJrKE2adFj22DMwjWKj6 +0pZU1Ushv4r7eb1NmFfl7a6Pz5ert+O5Qt53feK30+yiZF+Pgsx46SVTGy8QvicxhDhChdJ7ugW2 +Vbz8dXDT9gv1E5hLl2BiuxxZHtMMTitO3bCtQcM/YwUeFljZZYd1FwxtgolnA5IUcHomIEQ5Xw6X +dCYGNkVjenb8aLBfi/ZZ84LHQjSbo3b87KP7syeEH2uuFJ2W8ZwGfUCll84gF+lYiLO6BQk8psIR +aRqnPfdjeuYg0ZLhdNV2Gu6GTNYMSrGLJ4vafAoIoMOifeIfK/DjN0XpfUIYwrM3UIvssEaLyE0L +i30PN5bpmmyfj5EDkJj9DqHzBly1kc20ciEtVCwOUijhQr4UjjfPiJFyed1/yndY1z/L85iATcsb +mwAw/wOyHKge/mlVztXV2H8DywcLV8Kbo5/ZZzcdKhDgL9URosQ5bMeYDPWwPsS02exHFl150dpR +p6MmeSCFPiQQjDrM3nWXLv/uemBE1IgX5q2eW6kJbSvRn519O3OrFEs2NBMEgvE3mIvewNlxFbDj +96Oj54Zh3rVtYu/g9yo2Bb2uf9gpOGS6TxrqN3aP5FigZzxkMCGFG8UOOFI7k2eQjMd8va5V8JTZ +ijWZgBjDB1YuQ1MAEQEAAQ== + +AcLDXAQAAQoABgUCWYuUigAKCRDUpVvql9g3IOobH/wLm7sfLu3A/QWrdrMB1xRe6JOKuOQoNEt0 +Vhg8q4MgOt1mxPzBUMGBJCcq9EiTYaUT4eDXSJL1OKFgh42oK5uY+GLsPWamxBY1Rg6QoESjJPcS +2niwTOjjTdpIrZ5M3pKRmxTxT+Wsq9j+1t4jvy/baI6+uO6KQh0UIMyOEhG+uJ8aJ2OcF3uV5gtF +fL1Y4Jr1Ir/4B2K7s8OhlrO1Yw3woB+YIkOjJ6oAOfQx5B/p1vK4uXOCIZarcfYX4XOhNgvPGaeL +O+NHk3GwTmEBngs49E8zq8ii8OoqIT6YzUd4taqHvZD4inTlw6MKGld7myCbZVZ3b0NXosplwYXa +jVL9ZBWTJukcIs4jEJ0XkTEuwvOpiGbtXdmDDlOSYkhZQdmQn3CIveGLRFa6pCi9a/jstyB+4sgk +MnwmJxEg8L3i1OvjgUM8uexCfg4cBVP9fCKuaC26uAXUiiHz7mIZhVSlLXHgUgMn5jekluPndgRZ +D2mGG0WscTMTb9uOpbLo6BWCwM7rGaZQgVSZsIj1cise05fjGpOozeqDhG25obcUXxhIUStztc9t +Z9MwSz9xdsUqV8XztEhkgfc7dh2fPWluiE9hLrdzyoU1xE6syujm8HE+bIJnDFYoE/Kw6WqIEm/3 +mWhnOmi9uZsMBErKZKO4sqcLfR/zIn2Lx0ivg/yZzHHnDY5hwdrhQtn+AHCb+QJ9AJVte9hI+kt+ +Fv8neohiMTCY8XxjrdB3QBPGesVsIMI5zAd14X4MqNKBYb4Ucg8YCIj7WLkQHbHO1GQwhPY8Tl9u +QqysZo/WnLVuvaruEBsBBGUJ7Ju5GtFKdWMdoH3YQmYHdxxxK37NPqBY70OrTSFJU5QT6PGFSvif +aMDg0X/aRj2uE3vgTI5hdqI4JYv1Mt1gYOPv4AMx/o/2q9dVENFYMTXcYBITMScUVV8NzmH8SNge +w7AWUPlQvWGZbTz62lYXHuUX1cdzz37B0LrEjh1ZC1V8emzfkLzEFYP/qUk1c4NjKsTjj5d463Gq +cn31Mr83tt5l7HWwP8bvTMIj98bOIJapsncGOzPYhs8cjZeOy0Q7EcvHjGRrj26CGWZacT3f0A0e +kb66ocAxV4nH1FDsfn8KdLKFgmSmW6SXkD2nqY94/pommJzUBF6s54DijZMXqHRwIRyPA8ymrCGt +t4shJh7dobC8Tg6RA84Bf9HkeqI97PQYFYMuNX0U59x2s0IQsOAYjH53NIf/jSPC4GDvLs7k+O76 +R2PJK1VN6/ckJZAb3Rum5Ak5sbLTpRAVHIAVU1NAjHc5lYUHhxXJmJsbw6Jawb9Xb3T96s+WdD3Y +062upMY95pr0ZPf1tVGgzpcVCEw7yAOw+SkMksx+ +` + + encodedGenericClassicModel = `type: model +authority-id: generic +series: 16 +brand-id: generic +model: generic-classic +classic: true +timestamp: 2017-07-27T00:00:00.0Z +sign-key-sha3-384: d-JcZF9nD9eBw7bwMnH61x-bklnQOhQud1Is6o_cn2wTj8EYDi9musrIT9z2MdAa + +AcLBXAQAAQoABgUCWYuXiAAKCRAdLQyY+/mCiST0D/0XGQauzV2bbTEy6DkrR1jlNbI6x8vfIdS8 +KvEWYvzOWNhNlVSfwNOkFjs3uMHgCO6/fCg03wGXTyV9D7ZgrMeUzWrYp6EmXk8/LQSaBnff86XO +4/vYyfyvEYavhF0kQ6QGg8Cqr0EaMyw0x9/zWEO/Ll9fH/8nv9qcQq8N4AbebNvNxtGsCmJuXpSe +2rxl3Dw8XarYBmqgcBQhXxRNpa6/AgaTNBpPOTqgNA8ZtmbZwYLuaFjpZP410aJSs+evSKepy/ce ++zTA7RB3384YQVeZDdTudX2fGtuCnBZBAJ+NYlk0t8VFXxyOhyMSXeylSpNSx4pCqmUZRyaf5SDS +g1XxJet4IP0stZH1SfPOwc9oE81/bJlKsb9QIQKQRewvtUCLfe9a6Vy/CYd2elvcWOmeANVrJK0m +nRaz6VBm09RJTuwUT6vNugXSOCeF7W3WN1RHJuex0zw+nP3eCehxFSr33YrVniaA7zGfjXvS8tKx +AINNQB4g2fpfet4na6lPPMYM41WHIHPCMTz/fJQ6dZBSEg6UUZ/GiQhGEfWPBteK7yd9pQ8qB3fj +ER4UvKnR7hcVI26e3NGNkXP5kp0SFCkV5NQs8rzXzokpB7p/V5Pnqp3Km6wu45cU6UiTZFhR2IMT +l+6AMtrS4gDGHktOhwfmOMWqmhvR/INF+TjaWbsB6g== +` +) + +var ( + genericAssertions []asserts.Assertion + genericStagingAssertions []asserts.Assertion + genericExtraAssertions []asserts.Assertion + + genericClassicModel *asserts.Model + genericStagingClassicModel *asserts.Model + genericClassicModelOverride *asserts.Model +) + +func init() { + genericAccount, err := asserts.Decode([]byte(encodedGenericAccount)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s account: %v`, err)) + } + genericModelsAccountKey, err := asserts.Decode([]byte(encodedGenericModelsAccountKey)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s "models" account-key: %v`, err)) + } + + genericAssertions = []asserts.Assertion{genericAccount, genericModelsAccountKey} + + a, err := asserts.Decode([]byte(encodedGenericClassicModel)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s "generic-classic" model: %v`, err)) + } + genericClassicModel = a.(*asserts.Model) +} + +// Generic returns a copy of the current set of predefined assertions for the 'generic' authority as used by Open. +func Generic() []asserts.Assertion { + generic := []asserts.Assertion(nil) + if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + generic = append(generic, genericAssertions...) + } else { + generic = append(generic, genericStagingAssertions...) + } + generic = append(generic, genericExtraAssertions...) + return generic +} + +// InjectGeneric injects further predefined assertions into the set used Open. +// Returns a restore function to reinstate the previous set. Useful +// for tests or called globally without worrying about restoring. +func InjectGeneric(extra []asserts.Assertion) (restore func()) { + prev := genericExtraAssertions + genericExtraAssertions = make([]asserts.Assertion, len(prev)+len(extra)) + copy(genericExtraAssertions, prev) + copy(genericExtraAssertions[len(prev):], extra) + return func() { + genericExtraAssertions = prev + } +} + +// GenericClassicModel returns the model assertion for the "generic"'s "generic-classic" fallback model. +func GenericClassicModel() *asserts.Model { + if genericClassicModelOverride != nil { + return genericClassicModelOverride + } + if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + return genericClassicModel + } else { + return genericStagingClassicModel + } +} + +// MockGenericClassicModel mocks the predefined generic-classic model returned by GenericClassicModel. +func MockGenericClassicModel(mod *asserts.Model) (restore func()) { + prevOverride := genericClassicModelOverride + genericClassicModelOverride = mod + return func() { + genericClassicModelOverride = prevOverride + } +} diff --git a/asserts/sysdb/staging.go b/asserts/sysdb/staging.go new file mode 100644 index 00000000..c26a48a4 --- /dev/null +++ b/asserts/sysdb/staging.go @@ -0,0 +1,183 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build withtestkeys withstagingkeys + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package sysdb + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" +) + +const ( + encodedStagingTrustedAccount = `type: account +authority-id: canonical +account-id: canonical +display-name: Canonical +timestamp: 2016-04-01T00:00:00.0Z +username: canonical +validation: certified +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcLBXAQAAQoABgUCV640ggAKCRAHKljtl9kuLrQtEADBji8VwAuislurkFORTmcXV/DOkvyvAYEN +mB/MLniK4MlLX+RDncDBmF38IK9SRkxbwwJuKgvsjwsYJ3w1P7SGvVfNyU2hLRFtdxDMVC7+A9g3 +N1VW9W+IOWmYeBgXiveqAlSJ9GUvLQiBgUWRBkbyAT6aLkSZrTSjxGRGW/uoNfjj+CbAR4HGbRnn +IOxDuQyw6rOXQZKfZvkD1NiH+0QzXLv0RivE8+V5uVN+ooUFRoVQmqbj7orvPS9iTY5AMVjCgfo0 +UiWiN6NyCfDBDz0bZhIZlBU4JF5W0I/sEwsuYCxIhFi5uPNmQXqqb5d9Y3bsxIUdMR0+pai1A3eI +HQmYX12wCnb276R5Adz4iol19oKAR2Qf3VJBvPccdIFU7Qu5FOOihQdMRxULBBXGn1HQF1uW+ue3 +ZQ3x6e8s3XjdDQE/kHCDUkmzhbk1SErgndg6Q1ipKJ+4G6dOc16s66bSFA4QzW53Y40NP0HRWxe2 +tK9VOJ+z9GvGYp5H1ZXbbs78t9bUwL7L6z/eXM6BRho6YY9X7nImpByIkdcV47dCyVFol6NrM5NS +NSpdtRStGqo7tjPaBf86p2vLOAbwFUuaE3rwf5g/agz4S/v5G5E2tKmfQs6vGYrfVtlOzr8gEoXH ++/hOEC3wYEJjpXmFRjUjJwr0Fbej2TpoITpfzbySpg== +` + encodedStagingRootAccountKey = `type: account-key +authority-id: canonical +revision: 3 +public-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu +account-id: canonical +name: staging-root +since: 2016-04-01T00:00:00.0Z +body-length: 717 +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcbBTQRWhcGAARAA4wh+b9nyRdZj9gNKuHz8BTNZsLOVv2VJseHBoMNc4aA8EgmLwMF/aP+q1tAQ +VOeynhfSecIK/2aWKKX+dmU/rfAbnbdHX1NT8OnG2z3qdYdqw1EreN8LcY4DBDfa1RNKcjFvBu+Q +jxpU289m1yUjjc7yHie84BoYRgDl0icar8KF7vKx44wNhzbca+lw4xGSA5gpDZ1i1smdxdpOSsUY +WT70ZcJBN1oyKiiiCJUNLwCPzaPsH1i3WwDSaGsbjl8gjf2+LFNFPwdsWRbn3RLlFcFbET2bFe5y +v6UN+0cSh9qLJeLR2h0WDaVBp5Gx4PAYAfpIIF8EH3YbvI8uuTmBza8Ni0yozOZ2cXCSdezLGW2m +b6itOq/taBhgl8gzhKqki9jAOWmDBeBIbe2rUuNJrfHVH8+lWTzuzJIcHSHeAjFG1xid+HOOsw0e +Ag3JMjJaqCGCp0Oc9/WBtHV6jB30jLzht5QjJZ6izIKswRrvt0nCowp74FZ1l1ekXZPhhkA5MBMb +AoTiz9UvRZAWBPa5gX4R7eaekGjCPWI8NpJ7pT3Xh3NIHIsjyf0JcysoH2V1+A9qT1LOCyczf1Uc +9d8PXap1zhhQuczZcnw7vAwLEIwldfp08x6klsgiP6jqIB4XKJCjBDu/gn682ydWzfLT8echVHpg +uI62X67Ns1ZbFWMAEQEAAQ== + +AcLBXAQAAQoABgUCV86jSgAKCRAHKljtl9kuLpV6EADO8Q1WKJwoTfeIpBpQfDhdhqJLmW86Qrjq +P9ZsndN8eA4uSbo08yg9jxi6Q3J/A5QK6rhTz5Nu41frKVpgFr80BpIx8cHsY2dZNyKCm70Jjy4h +cxteK7mwdAzdWG/Dg7Nr4fhOmpepsh1gIXvjWhTkT226DIO6l45o6N2hMKKkWmqJYqVD6i7UE4Ed +xmC+IoluhnKGGwM6JpyOw0RViXbLjVDR58n4q1xmK7cFduMoLKszVY4/KGmKT8gA6D4pUOa62F84 +Ejh6akRum7uqygBibYT/DP+KA+MhHvpQ8XZem7IVIEnMJr7U2gde3brbVr0oiCl7FzfiBNy6qw92 +cTsE8o3JV0Lc106SWU28GuWPgyXjoH8imzSmWlpQtlPlKEDwMQt31XDKUKp0ZKiEax3cQ6VjMv1f +PV3bHNjD+tBq5e1xm/UWyGu7J2N4VPLgUK7F4TPUJk5lwKjmII8KD3KA/IeHnZVN6vmC2nKfhGvw ++rJllQQ0IWY9RfIdzFHpVvthe48g27ok5yEgovAc/s7xWZ6CBgyzYWLQMNFvENj04CzGvxirKwuJ +Fy5UJIEKB0j0R2qnCz6HZkyQrUsz5HiIIlks18FfOZwuIc4GGPbwwQBoXW7a6KQg0aa62BPj5Iww +3w60rtTSUsjINkZ/GXLodfzPglOl6VLF7bWx2hGesg== +` + encodedStagingGenericAccount = `type: account +authority-id: canonical +account-id: generic +display-name: Generic +timestamp: 2017-07-27T00:00:00.0Z +username: generic +validation: certified +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcLBXAQAAQoABgUCWXmmFAAKCRAHKljtl9kuLkAWD/98LgECwAN8S09o4aEFpdGXgWpx8z58wl6T +5mZVDyYpCV9ugC2DqBqGQxp4X1P7Wn9+weXw8nmL7IywVn/hCVHJOmBLJSr3wLjpVBY9RrIHYoXi +k9W7IFo4ggw1j1FRLg2tKk81MnK0fK/Qws9OXzilDir5R2bQ/E0sodGW3NpbwtbpkY/BtP6YPoJ/ +1+205KG5m6oG8y6mf74bjMGfJ+iFFpIDayIpXl+YTkJ25BOVGcuC66cIrmdc63rBIHL2tU/3GUMB +xZGiyG9Fuli1uV4ALhN9j43hxAtVwXOn/qgOiN8TGQz3OvlVUXTuFVmkdvCdfT2XHrJjFmEs9SlL +u2EEmvaNFJ61lQG/VrN6O0BswenTlIO0tTFe126o/cTmKg8/ga4v2WjMlcOCzfu+cIZIzTTnn4Le +iXdQ6+c3QN+Co4SI0UvgJ4nGWQ9W+4q4xVJTliKTzK2BZ40vHUi51rMC/puqsMpnAbHSn4iy8vpf +CyJh7jyuITPEzfpurNMb+VD+1Brd2DJCVnlwQq+rzNerXd5xcHCdZsfX+ATukHgYTZWa467ZEFhI +Bk1xUWAYs8r2JDFb5YPtZuW7Vt1UUpFdx6DroL6OODvZ6mDUtsOa8nm7G1l4uRJtqunplPyCDjnL +aQhlAouLMltWeGITO+5jePHJKTnYQAFEvo0WIgEYpA== +` + encodedStagingGenericModelsAccountKey = `type: account-key +authority-id: canonical +public-key-sha3-384: 93jDrIGOXymDg9BPCLES5mAr6aGXU7e0wwXeJlIYIWbUzM_kB81CiqX7cTlB9Y1z +account-id: generic +name: models +since: 2017-07-27T00:00:00.0Z +body-length: 717 +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcbBTQRWhcGAARAAxcyFC13COEmIwWwLsjp4AAILhWSp8/dQ6cOzY3T7tqqoSn9iKyidpJTfrtml +DKHZe0zC10fog2Mvp1AO7dNqK9kHUdCQE+YatHmkm1a3QoZqwJsj77w09Q+l1uvDjrfrF0S/KcYa +0hfDZQ+51T1msbatWN2qU42dX280IMV+zo1GpKK8z6br4glY2tki4CJokVAAt+bl4bBqDZ4EoBYe +9CsACmNhw2d/fOAlis2jwG3tWXMORX9FcGRx/COasvRb7rjA0DJfKxOnTw7uC0UjDUB6bU6O0smS +Q5oK3V5fJIAcMBXNe5MkdTKGLY61hTFLiw4F6MkrM3O8dnXtexCojV+QtROTIM4R2dJTOv7r2s/9 +KT4wIQmOcjMEWxyq1H1rqjCHBjGnKa6GC1j/4NwqlxUEiqZMYs12px9ypeEqjL3tKURCanPOlwXO +p2E1i+V53XznnS3RA7I6Aa37w/9clTJk5vzVT8G6+k6xsB9zwKYOsipG0zHjyuW9Qtkd15bA0Iv9 +MrZGE4U7RwEnt4jBa98rcLs1sCkJEau0hEU4MiPyqi8XL2b/TtPnCwN8rQRVQvakzGw83Ol/B8ZI +2OGu0aB6HAWbdy81yXIUES9ZtH7nK5X7dSdJu92wXBMOyel9cryHzlYFjSlPKyqRx2lsYk4K6Hiq +VRY3L12yjXkcHcsAEQEAAQ== + +AcLBXAQAAQoABgUCWXmevAAKCRAHKljtl9kuLltVEADAfpBlY4b4oImKPq8Pp6UKFjgcMVjJLcSI +EOfAMygIaZwzNSuOh2wPRBAMMZlcFlBEFLfGbh7R2RG1/R7PSR4q+gMZZ3qJ5QUjUUuGnkSfCLhK +jVtPlX8qdPWTdEgUeTEKNzHogP0MiIChHdeuv/iQ9fSgdw/lBZsblKAdrHv00ZQHup9XGWdZ4Fnb +cSiK9tZ3/nZAG18PErEh7wJntwygqcjScS0jTSa5BecQoy8O6wxKafQgxuixdHw+dt6sa34qzwel +ROb1VmNcmMGsv2YuPsRcqjgvL7drDzXRRcYhmiSCUFhGPx3RY0UWO9G9Pzok64l/1D7o6Xah3h4D +oxkepM5JTAiy165kfQzEFGMtvlv0d3mOCLMWqJzjzhhn/bPcoh5MO/PhpSR1y71tjjWtKR4SD1K/ +feR+KE73gEgqmHLss3TF/O2RxvbV15W0paxmiffUyeE/uQ1p5ddmBwke/gM/OOgUA4G3g9vgTeaQ +YVFCD0h75mX9GylVXUMmxlYSRVO59JsxCpWSqXWh4xkigbBZSAOPn6vkM/4nxZLf/ufVyNqnwo2r +Si/lWgk/lJoHaTVsqm9n0DxiR8lH54eFghprkN+KgGFMKlY127n50CEwqG1j4gKfjxmRycgKbx5O +a0IGmGjVnVmGOdX9wpg6fBHbhczXZtId02Q7yzF87A== +` + + encodedStagingGenericClassicModel = `type: model +authority-id: generic +series: 16 +brand-id: generic +model: generic-classic +classic: true +timestamp: 2017-07-27T00:00:00.0Z +sign-key-sha3-384: 93jDrIGOXymDg9BPCLES5mAr6aGXU7e0wwXeJlIYIWbUzM_kB81CiqX7cTlB9Y1z + +AcLBXAQAAQoABgUCWXnYbAAKCRDqhmvwxUsbelvOD/4qxiDs4blJoRSXmzvsKTyM/Z2QuLw9bqUj +QXKoCSB78ATFwr01kvqJMzwJ1eT4zKOajUERKPN9fN1af0w07DoYG5bt/Pb7s/UFDmwIQg244wLI +lQ/NPCAm4SEvN1GEe0OxdCpMuPe+x++FvFtnF7CXJPmLdHln6A1eMhwyxGX+el1QxhiR+mWLCCNp +B4ndjh154H5SXRw1lmUiYdE/kCsOqGeZ5ljTni+Rh8xDYxmVCthrLCUVtHhMVrKeylDwwS7Sf/HV +GY9r/C9r07xRom06bBN/vQwdoLzGuU3SS7UsN0Ud95tJhAUtP5jW1dN8otviMcOAdtj7jTwSX4FY +pdgmkldjaCRaHxBA923cjGgl98LCjbdG5KmmKoT6DTb3AyFOT2XwlRl/MaRJBK2Tp1nVNZDjLY4j +VfRETt17ZCONt3yn/OhQk8bV6EsdJvT2/nMlNejXgnMtLfbH8v6xWLKrLOVOjILVF5zgK8+z4+d2 +ILIZupGooMouhddmcHem76lSnS+y75NMQXg5lBrUU2xAQRloWTw0oF+Hr5vcZkX5f4R/yH8Zz1Dt ++zRs2zqOK5hjdejhU5x/N3KSBLy+TUMk7JsdVv0nhdpJUKrFyGWn+YzBNE2GgEfPfXnkaU91/AD2 +SWyt8kWVPmT3DCzs7u5IXYIVxcq4FjkmeU9sTrn88g== +` +) + +func init() { + stagingTrustedAccount, err := asserts.Decode([]byte(encodedStagingTrustedAccount)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + stagingRootAccountKey, err := asserts.Decode([]byte(encodedStagingRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + trustedStagingAssertions = []asserts.Assertion{stagingTrustedAccount, stagingRootAccountKey} + + genericAccount, err := asserts.Decode([]byte(encodedStagingGenericAccount)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s account: %v`, err)) + } + genericModelsAccountKey, err := asserts.Decode([]byte(encodedStagingGenericModelsAccountKey)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s "models" account-key: %v`, err)) + } + + genericStagingAssertions = []asserts.Assertion{genericAccount, genericModelsAccountKey} + + a, err := asserts.Decode([]byte(encodedStagingGenericClassicModel)) + if err != nil { + panic(fmt.Sprintf(`cannot decode "generic"'s "generic-classic" model: %v`, err)) + } + genericStagingClassicModel = a.(*asserts.Model) +} diff --git a/asserts/sysdb/sysdb.go b/asserts/sysdb/sysdb.go new file mode 100644 index 00000000..66ef9cbe --- /dev/null +++ b/asserts/sysdb/sysdb.go @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +// Package sysdb supports the system-wide assertion database with ways to open it and to manage the trusted set of assertions founding it. +package sysdb + +import ( + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/dirs" +) + +func openDatabaseAt(path string, cfg *asserts.DatabaseConfig) (*asserts.Database, error) { + bs, err := asserts.OpenFSBackstore(path) + if err != nil { + return nil, err + } + keypairMgr, err := asserts.OpenFSKeypairManager(path) + if err != nil { + return nil, err + } + cfg.Backstore = bs + cfg.KeypairManager = keypairMgr + return asserts.OpenDatabase(cfg) +} + +// Open opens the system-wide assertion database with the trusted assertions set configured. +func Open() (*asserts.Database, error) { + cfg := &asserts.DatabaseConfig{ + Trusted: Trusted(), + OtherPredefined: Generic(), + } + return openDatabaseAt(dirs.SnapAssertsDBDir, cfg) +} diff --git a/asserts/sysdb/sysdb_test.go b/asserts/sysdb/sysdb_test.go new file mode 100644 index 00000000..0d0e960c --- /dev/null +++ b/asserts/sysdb/sysdb_test.go @@ -0,0 +1,212 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package sysdb_test + +import ( + "os" + "path/filepath" + "syscall" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" +) + +func TestSysDB(t *testing.T) { TestingT(t) } + +type sysDBSuite struct { + extraTrusted []asserts.Assertion + extraGeneric []asserts.Assertion + otherModel *asserts.Model + probeAssert asserts.Assertion +} + +var _ = Suite(&sysDBSuite{}) + +func (sdbs *sysDBSuite) SetUpTest(c *C) { + tmpdir := c.MkDir() + + pk, _ := assertstest.GenerateKey(752) + + signingDB := assertstest.NewSigningDB("can0nical", pk) + + trustedAcct := assertstest.NewAccount(signingDB, "can0nical", map[string]interface{}{ + "account-id": "can0nical", + "validation": "certified", + "timestamp": "2015-11-20T15:04:00Z", + }, "") + + trustedAccKey := assertstest.NewAccountKey(signingDB, trustedAcct, map[string]interface{}{ + "account-id": "can0nical", + "since": "2015-11-20T15:04:00Z", + "until": "2500-11-20T15:04:00Z", + }, pk.PublicKey(), "") + + sdbs.extraTrusted = []asserts.Assertion{trustedAcct, trustedAccKey} + + otherAcct := assertstest.NewAccount(signingDB, "gener1c", map[string]interface{}{ + "account-id": "gener1c", + "validation": "certified", + "timestamp": "2015-11-20T15:04:00Z", + }, "") + + sdbs.extraGeneric = []asserts.Assertion{otherAcct} + + a, err := signingDB.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "brand-id": "can0nical", + "model": "other-model", + "classic": "true", + "timestamp": "2015-11-20T15:04:00Z", + }, nil, "") + c.Assert(err, IsNil) + sdbs.otherModel = a.(*asserts.Model) + + fakeRoot := filepath.Join(tmpdir, "root") + + err = os.Mkdir(fakeRoot, os.ModePerm) + c.Assert(err, IsNil) + dirs.SetRootDir(fakeRoot) + + sdbs.probeAssert = assertstest.NewAccount(signingDB, "probe", nil, "") +} + +func (sdbs *sysDBSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +func (sdbs *sysDBSuite) TestTrusted(c *C) { + trusted := sysdb.Trusted() + c.Check(trusted, HasLen, 2) + + restore := sysdb.InjectTrusted(sdbs.extraTrusted) + defer restore() + + trustedEx := sysdb.Trusted() + c.Check(trustedEx, HasLen, 4) +} + +func (sdbs *sysDBSuite) TestGeneric(c *C) { + generic := sysdb.Generic() + c.Check(generic, HasLen, 2) + + restore := sysdb.InjectGeneric(sdbs.extraGeneric) + defer restore() + + genericEx := sysdb.Generic() + c.Check(genericEx, HasLen, 3) +} + +func (sdbs *sysDBSuite) TestGenericClassicModel(c *C) { + m := sysdb.GenericClassicModel() + c.Assert(m, NotNil) + + c.Check(m.AuthorityID(), Equals, "generic") + c.Check(m.BrandID(), Equals, "generic") + c.Check(m.Model(), Equals, "generic-classic") + c.Check(m.Classic(), Equals, true) + + r := sysdb.MockGenericClassicModel(sdbs.otherModel) + defer r() + + m = sysdb.GenericClassicModel() + c.Check(m, Equals, sdbs.otherModel) +} + +func (sdbs *sysDBSuite) TestOpenSysDatabase(c *C) { + db, err := sysdb.Open() + c.Assert(err, IsNil) + c.Check(db, NotNil) + + // check trusted + _, err = db.Find(asserts.AccountKeyType, map[string]string{ + "account-id": "canonical", + "public-key-sha3-384": "-CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk", + }) + c.Assert(err, IsNil) + + trustedAcc, err := db.Find(asserts.AccountType, map[string]string{ + "account-id": "canonical", + }) + c.Assert(err, IsNil) + + err = db.Check(trustedAcc) + c.Check(err, IsNil) + + // check generic + genericAcc, err := db.Find(asserts.AccountType, map[string]string{ + "account-id": "generic", + }) + c.Assert(err, IsNil) + _, err = db.FindMany(asserts.AccountKeyType, map[string]string{ + "account-id": "generic", + "name": "models", + }) + c.Assert(err, IsNil) + + err = db.Check(genericAcc) + c.Check(err, IsNil) + + err = db.Check(sysdb.GenericClassicModel()) + c.Check(err, IsNil) + + // extraneous + err = db.Check(sdbs.probeAssert) + c.Check(err, ErrorMatches, "no matching public key.*") +} + +func (sdbs *sysDBSuite) TestOpenSysDatabaseExtras(c *C) { + restore := sysdb.InjectTrusted(sdbs.extraTrusted) + defer restore() + + db, err := sysdb.Open() + c.Assert(err, IsNil) + c.Check(db, NotNil) + + err = db.Check(sdbs.probeAssert) + c.Check(err, IsNil) +} + +func (sdbs *sysDBSuite) TestOpenSysDatabaseBackstoreOpenFail(c *C) { + // make it not world-writeable + oldUmask := syscall.Umask(0) + os.MkdirAll(filepath.Join(dirs.SnapAssertsDBDir, "asserts-v0"), 0777) + syscall.Umask(oldUmask) + + db, err := sysdb.Open() + c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") + c.Check(db, IsNil) +} + +func (sdbs *sysDBSuite) TestOpenSysDatabaseKeypairManagerOpenFail(c *C) { + // make it not world-writeable + oldUmask := syscall.Umask(0) + os.MkdirAll(filepath.Join(dirs.SnapAssertsDBDir, "private-keys-v1"), 0777) + syscall.Umask(oldUmask) + + db, err := sysdb.Open() + c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") + c.Check(db, IsNil) +} diff --git a/asserts/sysdb/testkeys.go b/asserts/sysdb/testkeys.go new file mode 100644 index 00000000..7b615645 --- /dev/null +++ b/asserts/sysdb/testkeys.go @@ -0,0 +1,30 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build withtestkeys + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package sysdb + +import ( + "github.com/snapcore/snapd/asserts/systestkeys" +) + +// init will inject the test trusted assertions when this module build tag "withtestkeys" is defined. +func init() { + InjectTrusted(systestkeys.Trusted) +} diff --git a/asserts/sysdb/trusted.go b/asserts/sysdb/trusted.go new file mode 100644 index 00000000..9cd79640 --- /dev/null +++ b/asserts/sysdb/trusted.go @@ -0,0 +1,156 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package sysdb + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" +) + +const ( + encodedCanonicalAccount = `type: account +authority-id: canonical +account-id: canonical +display-name: Canonical +timestamp: 2016-04-01T00:00:00.0Z +username: canonical +validation: certified +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcLDXAQAAQoABgUCV7UYzwAKCRDUpVvql9g3IK7uH/4udqNOurx5WYVknzXdwekp0ovHCQJ0iBPw +TSFxEVr9faZSzb7eqJ1WicHsShf97PYS3ClRYAiluFsjRA8Y03kkSVJHjC+sIwGFubsnkmgflt6D +WEmYIl0UBmeaEDS8uY4Xvp9NsLTzNEj2kvzy/52gKaTc1ZSl5RDL9ppMav+0V9iBYpiDPBWH2rJ+ +aDSD8Rkyygm0UscfAKyDKH4lrvZ0WkYyi1YVNPrjQ/AtBySh6Q4iJ3LifzKa9woIyAuJET/4/FPY +oirqHAfuvNod36yNQIyNqEc20AvTvZNH0PSsg4rq3DLjIPzv5KbJO9lhsasNJK1OdL6x8Yqrdsbk +ldZp4qkzfjV7VOMQKaadfcZPRaVVeJWOBnBiaukzkhoNlQi1sdCdkBB/AJHZF8QXw6c7vPDcfnCV +1lW7ddQ2p8IsJbT6LzpJu3GW/P4xhNgCjtCJ1AJm9a9RqLwQYgdLZwwDa9iCRtqTbRXBlfy3apps +1VjbQ3h5iCd0hNfwDBnGVm1rhLKHCD1DUdNE43oN2ZlE7XGyh0HFV6vKlpqoW3eoXCIxWu+HBY96 ++LSl/jQgCkb0nxYyzEYK4Reb31D0mYw1Nji5W+MIF5E09+DYZoOT0UvR05YMwMEOeSdI/hLWg/5P +k+GDK+/KopMmpd4D1+jjtF7ZvqDpmAV98jJGB2F88RyVb4gcjmFFyTi4Kv6vzz/oLpbm0qrizC0W +HLGDN/ymGA5sHzEgEx7U540vz/q9VX60FKqL2YZr/DcyY9GKX5kCG4sNqIIHbcJneZ4frM99oVDu +7Jv+DIx/Di6D1ULXol2XjxbbJLKHFtHksR97ceaFvcZwTogC61IYUBJCvvMoqdXAWMhEXCr0QfQ5 +Xbi31XW2d4/lF/zWlAkRnGTzufIXFni7+nEuOK0SQEzO3/WaRedK1SGOOtTDjB8/3OJeW96AUYK5 +oTIynkYkEyHWMNCXALg+WQW6L4/YO7aUjZ97zOWIugd7Xy63aT3r/EHafqaY2nacOhLfkeKZ830b +o/ezjoZQAxbh6ce7JnXRgE9ELxjdAhBTpGjmmmN2sYrJ7zP9bOgly0BnEPXGSQfFA+NNNw1FADx1 +MUY8q9DBjmVtgqY+1KGTV5X8KvQCBMODZIf/XJPHdCRAHxMd8COypcwgL2vDIIXpOFbi1J/B0GF+ +eklxk9wzBA8AecBMCwCzIRHDNpD1oa2we38bVFrOug6e/VId1k1jYFJjiLyLCDmV8IMYwEllHSXp +LQAdm3xZ7t4WnxYC8YSCk9mXf3CZg59SpmnV5Q5Z6A5Pl7Nc3sj7hcsMBZEsOMPzNC9dPsBnZvjs +WpPUffJzEdhHBFhvYMuD4Vqj6ejUv9l3oTrjQWVC +` + + encodedCanonicalRootAccountKey = `type: account-key +authority-id: canonical +revision: 2 +public-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk +account-id: canonical +name: root +since: 2016-04-01T00:00:00.0Z +body-length: 1406 +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcbDTQRWhcGAASAA4Zdo3CVpKmTecjd3VDBiFbZTKKhcG0UV3FXxyGIe2UsdnJIks4NkVYO+qYk0 +zW26Svpa5OIOJGO2NcgN9bpCYWZOufO1xTmC7jW/fEtqJpX8Kcq20+X5AarqJ5RBVnGLrlz+ZT99 +aHdRZ4YQ2XUZvhbelzWTdK5+2eMSXNrFjO6WwGh9NRekE/NIBNwvULAtJ5nv1KwZaSpZ+klJrstU +EHPhs+NGGm1Aru01FFl3cWUm5Ao8i9y+pFcPoaRatgtpYU8mg9gP594lvyJqjFofXvHPwztmySqf +FVAp4gLLfLvRxbXkOfPUz8guidqvg6r4DUD+kCBjKYoT44PjK6l51MzEL2IEy6jdnFTgjHbaYML8 +/5NpuPu8XiSjCpOTeNR+XKzXC2tHRU7j09Xd44vKRhPk0Hc4XsPNBWqfrcbdWmwsFhjfxFDJajOq +hzWVoiRc5opB5socbRjLf+gYtncxe99oC2FDA2FcftlFoyztho0bAzeFer1IHJIMYWxKMESjvJUE +pnMMKpIMYY0QfWEo5hXR0TaT+NxW2Z9Jqclgyw13y5iY72ZparHS66J+C7dxCEOswlw1ypNic6MM +/OzpafIQ10yAT3HeRCJQQOOSSTaold+WpWsQweYCywPcu9S+wCo6CrPzJCCIxOAnXjLYv2ykTJje +pNJ2+GZ1WH2UeJdJ5sR8fpxxRupqHuEKNRZ+2CqLmFC5kHNszoGolLEvGcK4BJciO4KihnKtxrdX +dUJIOPBLktA8XiiHSOmLzs2CFjcvlDuPSpe64HIL5yCxO1/GRux4A1Kht1+DqTrL7DjyIW+vIPro +A1PQwkcAJyScNRxT4bPpUj8geAXWd3n212W+7QVHuQEFezvXC5GbMyR+Xj47FOFcFcSZID1hTZEu +uMD+AxaBHQKwPfBx1arVKE1OhkuKHeSFtZRP8K8l3qj5W0sIxxIW19W8aziu8ZeDMT+nIEJrJvhx +zGEdxwCrp3k2/93oDV7g+nb1ZGfIhtmcrKziijghzPLaYaiM9LggqwTARelk3xSzd8+uk3LPXuVl +fP8/xHApss6sCE3xk4+F3OGbL7HbGuCnoulf795XKLRTy+xU/78piOMNJJQu+G0lMZIO3cZrP6io +MYDa+jDZw4V4fBRWce/FA3Ot1eIDxCq5v+vfKw+HfUlWcjm6VUQIFZYbK+Lzj6mpXn81BugG3d+M +0WNFObXIrUbhnKcYkus3TSJ9M1oMEIMp0WfFGAVTd61u36fdi2e+/xbLN0kbYcFRZwd9CmtEeDZ0 +eYx/pvKKaNz/DfUr0piVCRwxuxQ0kVppklHPO4sOTFZUId8KLHg28LbszvupSsHP/nHlW8l5/VK6 +4+KxRV2XofsUnwARAQAB + +AcLDXAQAAQoABgUCV83kkgAKCRDUpVvql9g3IA9hIADAkn4VXnJIFblhMSBe6hbTy7z6AfOhZxXR +Ds/mHsiWfFT6ifGi9SpZowhRX+ff57YvFCjlBqMYLKYE0NsFQYEUc5uBWiFZwC0ENydNhO23DV1B +elTSs6mr9duPm1eJAozFrQETOD1kz5BIamqBUeaTczjM+9l5i485Ffknbc+EaGOrtMEap0GqjByQ +u+ykZGvryVQ447avgjvFsMtA0quFi+SoW9PT/9D26e5rD7RIICYWG8mzFRn5Isqs/X4W1uAiKQe9 +pqHMbdNr/FCWX5ws0/nMaOq+b0z4EIIXIfT0JmIlFDQsAgFVnKwYw+zs32cTw4XuzvMhgMDtCowD +YodhiO/5AOMsMMV0qBsYxbIPJIEz7b6gwTYEJoTVkqTit6o3UgWrAy+p4Y7t0ickYIHgwiuKRS9E +fu0Ue+32NFp0XFqZElfXLK/U2yjto+fJXu6uAELsXesfFGIOp/nbRbNavUt9jAJeO7ftQczgf39T +YfA0OKerP5gAOd4+aO3gATPUjfWPsJ9908XC7QqK2BwS1kh/fMrd95mxcmXdF1bBElszKwaToBVQ +1m52EYp06kkPyOu+fGKFAoIMafcV/2Ztz1WMo/Vp0iP/r0WAtBDw6sDJyWOfRjUEvP7BBdEzraHV +VblbSrKzhYeEGdMDi6kFC+KEzfPDPFJX1l3saPBkz9VDuESbktyObQp9VfkFKYBgBnw3msQJk+6k +G4t0o3/DZ7qz/kTJXMogG26Z/FsMhPERsaLTbWRJ3WRyXX8COaTladSf8bG0Oib19outnjuvpjQ0 +qEV9eeGRBlx9mbidSYH95cj0zD2DKpeSZ83M5K1pFg+8RKToGElGTTk8vtdTfDVbmi3+QntfLq+z +ZMgs2+SmCWrV/MPC04Dl00CXywdKPyf6toomqRP7A5fS7W8P9fdPn+a8JCblcleGj9nvJXBQjue7 +97rofCEszhKhoE9fMCIUcSoTU9YAm5Jr+qclSEbV1pzwTvZ8auMIXtzEZV5n4aK4WPDV+lYCadrL +DlvJSJRuXRvIMbmvU9b8NxgG8AS88BkX3L9vlOpkMculwG1/iooQvxuFaJDargt370wAQo0lCpG3 +MxnsSusymwnYegvvvr7Xp/KBLZK1+8Djzm3fwAryp4qNo29ciVw3O9lFKmmuiIcxSY0bauXaK6kv +pTnYkmx7XGPF7Ahb7Ov0/0FE2Lx3JZXSEKeW+VrCcpYQOY++t67b+jf0AV4rZExcLFJzP6MPMimP +ZCd383NzlzkXK+vAdvTi40HPiM9FYOp6g8JTs5TTdx2/qs/SWFC8AkahIQmH0IpFBJep2JKl2kyr +FZMvASkHA9bR/UuXDvbMzsUmT/xnERZosQaZgFEO +` +) + +var ( + trustedAssertions []asserts.Assertion + trustedStagingAssertions []asserts.Assertion + trustedExtraAssertions []asserts.Assertion +) + +func init() { + canonicalAccount, err := asserts.Decode([]byte(encodedCanonicalAccount)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + canonicalRootAccountKey, err := asserts.Decode([]byte(encodedCanonicalRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + trustedAssertions = []asserts.Assertion{canonicalAccount, canonicalRootAccountKey} +} + +// Trusted returns a copy of the current set of trusted assertions as used by Open. +func Trusted() []asserts.Assertion { + trusted := []asserts.Assertion(nil) + if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + trusted = append(trusted, trustedAssertions...) + } else { + if len(trustedStagingAssertions) == 0 { + panic("cannot work with the staging store without a testing build with compiled-in staging keys") + } + trusted = append(trusted, trustedStagingAssertions...) + } + trusted = append(trusted, trustedExtraAssertions...) + return trusted +} + +// InjectTrusted injects further assertions into the trusted set for Open. +// Returns a restore function to reinstate the previous set. Useful +// for tests or called globally without worrying about restoring. +func InjectTrusted(extra []asserts.Assertion) (restore func()) { + prev := trustedExtraAssertions + trustedExtraAssertions = make([]asserts.Assertion, len(prev)+len(extra)) + copy(trustedExtraAssertions, prev) + copy(trustedExtraAssertions[len(prev):], extra) + return func() { + trustedExtraAssertions = prev + } +} diff --git a/asserts/systestkeys/trusted.go b/asserts/systestkeys/trusted.go new file mode 100644 index 00000000..eeb2f2a6 --- /dev/null +++ b/asserts/systestkeys/trusted.go @@ -0,0 +1,263 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +// Package systestkeys defines trusted assertions and keys to use in tests. +package systestkeys + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" +) + +const ( + TestRootPrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQcYBAAAAAEBEADx0Loc/418zmw2AIcf5uxC/hgshHyCU98n4cRfJph007X6gXJf +ifHsKlXlSa5NizsM9WlOgCI3eyekF088q7lQTORDo4YO5x/ZtmcAiePtbMrAac4D +9j+5Ax24jJ4VniYudQ1wX4x7wtXRpL+lCER0FS5HEQ6L3OW/SntfVtSzoshRO5u7 +r6yYW1t0EE04P7Squ+N/sK+xJytOxCzC2/BwugHgZf3jArpFCuWSZgk9QVmqR1a3 +tynSKrx35OzxSdPyyBa4XOQwKAEquK1Lv/njmYTwATR+zIUa3n7SNyOCz0sOTmBE +7sSCgUtc+wQF2It1Wazs4YDA8YbTTB8VgveGjg8J8qr6YfSQ6BQDKeUnvHwwJH3Z +5YSL/KUdeI7SOdFjxSy62szvp4s3jWJSVr/qPkNyxfFAH/HOViRR21e1iufov8NO +yeLFyW7eiA/OU8QXJXG/S9YiCQotZePYlFG3a6p7crfdO90XQf6bqydlNK2ftVje +J/1+/LHXj60qHXq5x1BrXPMmhMpOphZf0H5l8Q0YolSeFM/THsKbqWDcRQZrL9vm +GwDgMGipKG5/83SNUuiN2HGLcKT8ME2WoIPTPLi7O+KeNf5vhrL4soETc3XkCx8S +RYjDMj7U50OU5Zao7EmQzqWtDmFFDV8dmgKIaMduN4TVEgU7ZMDDa2nJRwARAQAB +AA/+PAQDZRYR/iNXXRHFd6f/BGN/CXF6W3hIfuP8MmdoWDqBRGKjSc35UpVxSx59 +2bYQGlfAYqDPnTh+Lq4wVs0CCcmDr7vilklLsOOh7dLLVI53RckcvgP8bcU1t6uC +wrfFHyujAbxdKAxDuCvs+p8yKiNloHK9yv2wscjhFNj+onToxayHKs5fhlLKQGSZ +XbgF9Yf7XyIxgMTJbVuoBlbC9p9bvt9hY1m2dFNPhgW4DlFtWSMqhR87DHPZ4eHZ +4srhhTSe2vQHGGKdY4aBUDcd5JyiD1UlO8Ez2ebV0AOqVxlutebC4ujlscQ4OaP9 +LBxCBIaUshgHthtbzI5sepDOMMYJKV0R0+gtW6+rrVaudeSdt62yLF6a8n5m41dP +6OxGmO84ejoyw/EMutrVeraoz2b5bb35gx9bLEMRFr8XL2x1Ckdx2epNTL9aOVmA +JiCMGC0zFyt/jbNXnoOjD8tzUj44jrJnY2PcnJHgDogXMoIRduPDnwYaQtXkffkW +zsVbdUHvMkZuKXUBfsxCwFYgGm2i9y0dGnTSzI03TevRJ1FM2+TN8uQ8h4/C0xfZ +snXgvVHAwAOJwE8onul8AiepE1ihSWmaQfq/2Hn+0u+wbIsdrpP9xKB88KvZtgVe +mXj1vbDHw1nbORH63vgzfT8tyIhvR1RfDutQoGKkrZ4ZCIkIAPgDABPYucbnUpv/ +e2OSKd+Z/RGwUqghtp6recs3+9IdIoz/XPQHr9eqmgMUSikRFHLD6s0unIUm1b5s +Q+98OvadsP0D5EaKjAo0Za2PQVi8Na3eoGDs+DpX2+lhq5lvYCezGNoo50awKhzs +vRE4RU91bohfNvfJ9bY0AwyrYHDg67Jl/JzWtPNBqfAMlRW5WM9NYvp+Brk8JJLU ++Ncf5w//7S4lH5qBf3rXk6ur8ittIq28MGalW7T8Uk2F7VkrvCDaKkWPP8jwux79 +u1F22ADPYbdHB2RUSv0FGPrOItUyl81V6qTpAqO8iYQVol+B0J95B7Z0DLa+QecH +vVfaVS8IAPmaokwf3mk36dmbHvDIaPjloD1Gw3PCPZ+dpmGLfvcPm4YcA/uTzbNV +E46QlTZCny8+5W4xDaetpdODXRvCciwnjJ/wcdpSaMe0R5Res8weIcV2RAM9UNNb +q6BiTDqyBwk/dmFYY71xus/tuAnxmhZnXrJYjcA1CEsO+cu3SkwYM6dp3d1W0Bfh +li4b6eT3bC7IRD+KW+3Vdti8bShoLUkK2UwXHhnz0yBBE+8vQc8PoxOwt29EcQDf +GGL1Tz31yxRF+EADH4SL5ypUZFUctLkJ76WP9vNHqx5Tzrbt2aHqqbtvkxfzcB/m +k6cm8XzLVxttNHvZkvjwtvl76+X8d2kH/34hjWibosJueZb7HoFuJIoXXtPJ+sY5 +MSnY9+uGW4FgzgyUjWd5bfBCcCOGIqJFj37YVJwPKXaXBr0CzgaeJfLNRqz9Mt6d +OyqYLdb4ojvFSvhfN7bjAiBbwTbGVsOVVKgiNYudWH5lBS9yqxKyDQeUmwSmgaWa +Y1zMmK7J/syCqMBlizox3NIjGUsV7JGHzatSGksblTdTHTts3D52yTphonZueYVz +f27546ta7Fk9uEts8XVrs8YiJgZw8DHEugmuD5ZFb5WrpF96jqpaAuEhUye0fkfA +GvRP9FpVShfxVockrCrLgCaaDs+/kg7cZS+PDU8uLlXnsKqXvkkH7ip/irQOICh0 +ZXN0cm9vdG9yZymJAjgEEwECACIFAgAAAAECGy8GCwkIBwMCBhUIAgkKCwQWAgMB +Ah4BAheAAAoJEExxmnn3gXGkIyAQAMmpCPsk3FjfH2wHMxDozPZJmgoPwFBj4VEi +Qg4pp1pWtTHWPm7qN2bUL0WaJkvdPvvana7T5iGSlQHAjQRgPQfS42+0Nz17AInR +QbpovdE3S/02UOWaF+VgFrF7IKHQhbxbfmjPBQAr/9mWfe/JGyUqlc14a8IwxOmf +k4qf3WVj48NI6PdtMYpBKtSpghc7rKQwFLyxEauoBtoF6VLyhha7TFBGGM3LJ5uU +SPr8oVCybkZ9xbWdfcodbe3Ix/gbG1rvX7Jp/pIlG+7DVKn/0xkR7zPPfDmZOBGd +VFdg9X8L9+QH00Rverp0cCZ+fN97W13/Mb2/E9Px0y86Omwyhg5SVbikemmybrK8 +JHelbZ2NMmN7YHq2TB1idii30aX/1PN9jGyHHFMWPj2BJmK2aWhN0QSX8sxCoS9O +NCXwYU5hfRX5RjyWnI51XDhhfpMikqXnLrxzmPme4htaIqMl332MiqusFZ0D6UVw +Br2jeRhncvRrsscvAibbUWgbN6u70xBGjZZksvT8vkBipkikXWJ8SPm5DBfbRe85 +NnAkj2flf8ZFtNwrCy93JPVqY7j4Ip5AHUqhlUhYyPEMlcPEiNIhqZFUZvMYAIRL +68Hgqm/HlvtVLR/P7H6mDd7XhVFT5Qxz3f+AD+hmQFf8NN4MDbhCxjkUBsq+eyGG +97WP6Yv2 +=gJ0v +-----END PGP PRIVATE KEY BLOCK----- +` + + encodedTestRootAccount = `type: account +authority-id: testrootorg +account-id: testrootorg +display-name: Testrootorg +timestamp: 2016-08-11T18:30:57+02:00 +username: testrootorg +validation: certified +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcLBUgQAAQoABgUCV6yoQQAAelEQAEdSECpdmV5a2G5VMBzJFuHQUU1FzgZ7gPQjc3l0BibDWm8O +rDi7IT3L80OkqS2AoQgHS5KtEvKqEmhyfcdzcXgvCkHR5kucRBJJaPy8z6gGMhzZIPlc+EqY+Cvb +/MQPLvtYYvtAxq1vWz+aDGGwk2Z/dFUG+wofvNWodz400gYTZeFOCZwStBD84S7iY/3pMQgC3+SO +QMr/VI+bgmOukFqZL0cX4ReiuUs2W45V6EC81UGBjk+k7AVTEXMR1Xo8f0yiRzlLoEdKQMCOC45Q +n4eedjCToGRPFcktM0QhgfbcpPIQKHNqKGGvtQQXvW5PIZ7AS4rTfQScXTn1dqDsL/ZVdasvOpCP +5o4WvoWMoU8+Hm4n6ckw4sXn//PZIQrtnkp2DO+9JXXZasIPg4k1mvUQ5Kb9qCcBbaM+OO1izOoC +3PY8xHNQNfHNHwBMewhnU2NpdTS0mTepN/8iFsDT1vSZ28OE2hgbu1ltqx4AsRkCVyFFx6N6OYm2 +UDNozU9K5w0NY4u9HSTDz4KrBIalAaKY72CIUqeVsmAcYatXglbj7dVTZTw75M0v1thQiSoKFqHw +CHykZ6BJRgminY1FqOg7tvqTwzYM7lwaE3K8JpAyzie7v+OSLSxy1vlwUmT2lT+h1i28/w+r+R3Q +C0QC8xuHSvOv3YRtzKna3smAfRlB +` + + TestRootKeyID = "hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR" + + encodedTestRootAccountKey = `type: account-key +authority-id: testrootorg +public-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR +account-id: testrootorg +name: test-root +since: 2016-08-11T18:30:57+02:00 +body-length: 717 +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcbBTQRWhcGAARAA8dC6HP+NfM5sNgCHH+bsQv4YLIR8glPfJ+HEXyaYdNO1+oFyX4nx7CpV5Umu +TYs7DPVpToAiN3snpBdPPKu5UEzkQ6OGDucf2bZnAInj7WzKwGnOA/Y/uQMduIyeFZ4mLnUNcF+M +e8LV0aS/pQhEdBUuRxEOi9zlv0p7X1bUs6LIUTubu6+smFtbdBBNOD+0qrvjf7CvsScrTsQswtvw +cLoB4GX94wK6RQrlkmYJPUFZqkdWt7cp0iq8d+Ts8UnT8sgWuFzkMCgBKritS7/545mE8AE0fsyF +Gt5+0jcjgs9LDk5gRO7EgoFLXPsEBdiLdVms7OGAwPGG00wfFYL3ho4PCfKq+mH0kOgUAynlJ7x8 +MCR92eWEi/ylHXiO0jnRY8UsutrM76eLN41iUla/6j5DcsXxQB/xzlYkUdtXtYrn6L/DTsnixclu +3ogPzlPEFyVxv0vWIgkKLWXj2JRRt2uqe3K33TvdF0H+m6snZTStn7VY3if9fvyx14+tKh16ucdQ +a1zzJoTKTqYWX9B+ZfENGKJUnhTP0x7Cm6lg3EUGay/b5hsA4DBoqShuf/N0jVLojdhxi3Ck/DBN +lqCD0zy4uzvinjX+b4ay+LKBE3N15AsfEkWIwzI+1OdDlOWWqOxJkM6lrQ5hRQ1fHZoCiGjHbjeE +1RIFO2TAw2tpyUcAEQEAAQ== + +AcLBXAQAAQoABgUCV8656QAKCRBMcZp594FxpNWlEADQgBlROdBTHpdZ3/9BbasxenUC3VXusMeK +0DmnsHrsAsyVk6xiHQQ3hWxvXKWoDkDsOhUqcQTsDBcIaZ18+qwpQciyItd+w3d7SSJ+MKSUpwsB +NOdgw1ykj7l1M/W7xAAPscFoV1xVSk9+rsLYFYDe23R+ecyotSmF+4QHj5b+hXeVIOUaqQTl5xPC +h0zVYNIUWv42q4Z+hiBS8+8UJ0G+7z/27XORkGHY6TXCt0aph7s5egr8Lm+/jq7c95HVsa7DwSpv +SqPajRnlyLiHFXUYAUPEU9oDgPwtLsqUkFfrv1WZ3ja1rDexgKBta+8BRyCAq3gPcMAjhiHXdjoW +90p893l9N6K82RiEOO9ic0pEezjQldg97oU+ajXNm3ryns+HX6hRd39rpzIsrbVdbCqun4RwMbCM +EVxgC/cuxMGcS40Co3O8wG3H/WIWOqcRQfolQTexmyzQljYt9WyWJdXmtPtaMzQGbOqE/dIjOK9j +xvrghVU4kX6fJFwPi+azMrluHV+WGSVxPCuLW8o2aipjOd1/bUQCL5OwRuaEWuLCiV01J8H/JjWV +hL4gGVqEM2KEPIDwY2yqX36jE7uN9O+mIPnS4Tdj0JQ5ZD1qh34wv+4QvhgNeyP120nuS1ykO9X0 +A806uPC5QK1+cgRMUz8zJ0afDNwE/DvpBQvE5CIi9A== +` + + TestStorePrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQcYBAAAAAEBEACYmqZm+xLnwg1Oz5RD6N+jzfq8FLm2RT+GTtzSG5l7dKjaBz2R +om+OSOFnqDTT+QaiJ3DeLZaR0wSn4m29T1m196782f86qRJzcCnUoCaovg6WU9Ug +jwfr3DbOq+aj49yofRK8cBUSg4LZOhc/TAQecBmxtW7noAqvCkcOmk8Qi9pLqCWu +wRfUBek54wdktVG1+wEHp2Ute66VrVStIAtEUISNe2peo62jlWj0LynreUsHLX2J +/Pg6uJYAYGpm8V0i2ajxUg9dIN2AwwcGW7YxI0kdV+jrrKlu6izlCzo+VUBEAIsm +DOCmUjmwNvNe1XHk71DxgmPPg19TRY5Zg9a+YA1cN4w2LFaha+6LFi+xdobHqZ0P +seH+CLymuRCZnuDFbUwQ5X0lOECpiOOzZrIZUPvcQjawpjFXASDeIlOhD9wTPc7Z +TUd2ZiNB9EMmJfcYQ8Fde20Ots8zjZIcSWi6V2Yn4+QkMt2QaYDznFhSgQod0QUi +SMVK1BzI7kKTI1k3tIeIAjADgOkYyYUnbqZqpXMm6Iu+JyuLYVH+wlpIDbg3wdsa +d7eBJLtatJBL6Mp7chk9XLrg0Kga+taj8e9N6qwh+KEo8SlebxBW2M2G2RWfdF0h +SA5o1bIB+dnh1bVNUgBN744cPDZM3IiZOMTTHvmcvoHX9Guf71U/1LCG/wARAQAB +AA//R+eWwK9NGSa2XowwsE7qEaTcoAKj/t5iMEa4hce7ahBt/02qFRUUu1Zb3xvC +yJ5uIbmz1PxmFg/4AaMPUkQxYSxzp3CQcnN33izbiPRtQtVKykp2AgFjGh+JM5iL +9G1Ja5qDWYb2ZuLQpMpaadjHmA/6C2IR/9HJNvEAykCrQIClO0DfgJg7QgwG+N+g +fDNzbOv4cELPyb6dZKlnXKvcozPNQV0FodI93vZnnacbeXiNgbRNktc/n2uaQlMr +z5Wq7ODiWdLwqlDyDdnXVYehMUYPDWR+u41/yGNPBB1mNDi3L1OSPTuUHspfpEhA +JE8ue1DIMwPdQ8oDAJmlmUglxpP1dnR3Q3XqUbsJMT6kAdqc4OSXF+L+E9j7EiA1 +UaXiiK+srj/GWFFdKlSf1JLYX3kOvrH/M1xMB6cmUshuWDfiJUGz9rPhPOIAvK11 ++Gog6kV+0JJXBe7oWEf8oewONLg7KtU1sSlHeuECpR+Pi652wXnAMeeHFjeCirp+ +jRPla+oKhrYMfLxk+x2YgMK4usoY6Q/KNTcHNs/FeRpzt50OFIaRbKL/I/CY1pB8 +oakl45D0+c38+6MZVkbPwDRN5ixUJfHwSBwl5qFyF3abP/N0gJVsdfPO0QyDbihm +1yo5Tvihd7aUkfTAF+E2BkZLIfuY5kREENxY/EHceST20gEIAMOjPOwYkN+V25o+ +MSIj9EBq9xEMpddHilpVXNkRHF2i89CFCUCKcIGe7wROvrqxQSqVrEDET4ZU6iqB +zsaA5RD4Fia3+eoZjvy4563H54XX2Wp89Qs2T0PREems5UMoeho/kCzSKdnYhhll +kbekWEqZAOzyCaBjzu7YowjrcUuceUbiDSsh6ds4/goS4h1AO/oroYawZQhvUfaf +W7ExpOsxuFa7S4N7mLywpeGaWcOuZt3r/EfM4gHpJaEntgqhjfiEtEkfO4dGKiAU ++hg+LmVPyBjQnVhK5NXSBc/zXaXOWqrVEkqTEQcZ5WsmpcB9hzqZIaFw9cAF4PKh +xm1ZOnkIAMewViBcogHUEzzn9ZxTXKi45po45g5qxsoifNlN3ZfShdrxOjXjYos2 +UujGfN+gZN8vV4bnD3Q6CbpioBT7lTZhweZVRwx/eQa/yQv20ZewL/CJduME8DZj +rQtyy4MRBhaNf3A8Gvx/CXJZaIHYfldRJYIrq9OuK4ael3Zf0uZwm9AleT5baFz8 +T8iRlojlzhT2+xi+Y/yLCCYFESkxgdXPkhUfYkh/O5NPWxSXnohDgKAtKj4gDe2c +Qs/zUI5Q+p8qucWbcbASZurDthTD80G6zGYNWX0e/6k45k/tatf0zJGLZVww02uc +Kq6MVafir1FzkOPxq41zmie8zPTe7zcIAL4m/lnWww+jPxM+LffdtgDqOeRxjgo6 +MV3576MqUakeIGVfnlW7SJCyjN2mnf0JbzrVgv7XxEcZIJrIePutMqdKm1YAt2YR +1TuU/rsKpUQt+d8t9rWfCYd1xeSn6IdNtoBaMeu6vI13pV1dghPAnQyovUK0xzI6 +seLeVhTU3wG9zZHJBycyE8PDTqE3awEetYLGFkz6DruIjYwylYRPZwSC1xpPcirf +nkSAeE2U9nmnxDWUQNhWzFTazYr7QQAUzghX3Mf2ZYeoDBBqDg9lQMy2oUJrJtfv +vqmejP39c3+fJiXlT2k2o0V6B8aZTNVaRn00E3hE+e1Obaa1lV1EWxaDcrQUICh0 +ZXN0cm9vdG9yZyBzdG9yZSmJAjgEEwECACIFAgAAAAECGy8GCwkIBwMCBhUIAgkK +CwQWAgMBAh4BAheAAAoJEN2glF+93m+NRIEP/2AxZS9tmJ6l7oltpYTEhAQdytAE +eqahcBYIARSTgvy3YJlOzdKdIoYsGogVvNZ7ashaFCpQtNaNezI7Mhz5cuVoHyYl +hEctEXSeTNUmxNekdksoBm2QHfnxFHbKLV4Kvj7dlvMhNVbpaMe/qI1SykddGBvh +woEp2HnHe3lGhlU84+XopEijphI8BXQ2so8bA0jEcuDJOAEXtVzj14miP6nZCsDD +EKHriukohhCQQUZVm0VOKLfdoi4QuAWbehBmlrhcvRDLvcr6p7jY00803jvaGBjD +XmS0DT51tNg6W2COQ5xlM9+hjK5n6nyZdT/OYeu+TqtdnpHcZxsF7qKsUBbKeQtA +Abh0wqtD58Kqp9UTovMVho/+/VEH9+gpfpvrieQvjrpZki2ZVnEhqlINOVwCYH0j +wC5qKcFeUmHHGhE1ShMKypZvLgqfc0soK8vaz+njN4IYrsWaI0iCQmr6FfV7Q8Ih +XAcSt/73baWnQsiBWWgl+FOxChDfwEWZaGFgtzyjexLpbi1V+Usuwd0+pX3U/+A6 +uXw5t77PXE4nW73a8EDM2nkG5ru+KswmOC0G7ULB2Cs9UOWqN+XChdii+VC68MMK +O0gyQlMQf+OPtU18Nff7hfKGY1ZCUbCwvb/+bHBvzpjmtWEuIOwPC0CBgU9G9FcX +o7ZSZ/h/bUY1EjE2 +=Nc2M +-----END PGP PRIVATE KEY BLOCK----- +` + + TestStoreKeyID = "XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y" + + encodedTestStoreAccountKey = `type: account-key +authority-id: testrootorg +public-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y +account-id: testrootorg +name: test-store +since: 2016-08-11T18:42:22+02:00 +body-length: 717 +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcbBTQRWhcGAARAAmJqmZvsS58INTs+UQ+jfo836vBS5tkU/hk7c0huZe3So2gc9kaJvjkjhZ6g0 +0/kGoidw3i2WkdMEp+JtvU9Ztfeu/Nn/OqkSc3Ap1KAmqL4OllPVII8H69w2zqvmo+PcqH0SvHAV +EoOC2ToXP0wEHnAZsbVu56AKrwpHDppPEIvaS6glrsEX1AXpOeMHZLVRtfsBB6dlLXuula1UrSAL +RFCEjXtqXqOto5Vo9C8p63lLBy19ifz4OriWAGBqZvFdItmo8VIPXSDdgMMHBlu2MSNJHVfo66yp +buos5Qs6PlVARACLJgzgplI5sDbzXtVx5O9Q8YJjz4NfU0WOWYPWvmANXDeMNixWoWvuixYvsXaG +x6mdD7Hh/gi8prkQmZ7gxW1MEOV9JThAqYjjs2ayGVD73EI2sKYxVwEg3iJToQ/cEz3O2U1HdmYj +QfRDJiX3GEPBXXttDrbPM42SHElouldmJ+PkJDLdkGmA85xYUoEKHdEFIkjFStQcyO5CkyNZN7SH +iAIwA4DpGMmFJ26maqVzJuiLvicri2FR/sJaSA24N8HbGne3gSS7WrSQS+jKe3IZPVy64NCoGvrW +o/HvTeqsIfihKPEpXm8QVtjNhtkVn3RdIUgOaNWyAfnZ4dW1TVIATe+OHDw2TNyImTjE0x75nL6B +1/Rrn+9VP9Swhv8AEQEAAQ== + +AcLBXAQAAQoABgUCV866kwAKCRBMcZp594FxpHWHD/9AaZXqyT/Zsmq/VzmAMpd9JvCH4PHQKtAP +bXfP2Dnpa2wk2wuzQuSWunR8NDRyVh/aNVeTEZ9dFm/B8LR+U2O4rsHmFSeicmsTmo9u/HouRdEU +zeSc6cbAxMPpfNSjr5J+URLjGRT6oX5fEBmRPx/OC9pEIScMx7uKmTKEnuyMzLRNN/6HiGWKrFCo +nJdKkwRXrkCHyXWAOv1GumT7NDuyFcjAqt/UdHliTZkDBImKOsBmBVXMUjg7HCSS2uq/5WjStJ+B +JHQ4GSsXBvVINs6BncNWcvV6mCQ73D57MzGhqo997Zb4tSrn7UNGWK7GLCzV3e/pFlG7pw6HbgnQ ++rxU2Oj/TPVw0tcnUiRl2ttKpm+nua0Cl+MD+Gx0KXLAVp0ZGOQ9yGyP9AePFzcOR8SlRIgxi0EI +iJkSeYilqoKo3AJhnICRiqvAca2TGJoiJUryEgZ8jbTOElfaF2p+y0xvXGlWbKZm1gzGyvFM5fV5 +hJTlp/am+2uVn6U8wPACir4PrbuXYo7L4MIXww2OEO0ruBIaLARbc5IutSWmw6AEYQUxtsa9bdHV +Zin7LGbEj6lZm8GycWQwh4B6Vnt6dJRIyPc/9G7uM8Ds/2Wa7+yAxhiPqm8DwlbOYh1npw4X4TLD +IMGnTv5N3zllI+Xz4rqJzNTzEbvOIcrqWxCedQe79A== +` +) + +var ( + TestRootAccount asserts.Assertion + TestRootAccountKey asserts.Assertion + // here for convenience, does not need to be in the trusted set + TestStoreAccountKey asserts.Assertion + // Testing-only trusted assertions for injecting in the the system trusted set. + Trusted []asserts.Assertion +) + +func init() { + acct, err := asserts.Decode([]byte(encodedTestRootAccount)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + accKey, err := asserts.Decode([]byte(encodedTestRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted assertion: %v", err)) + } + storeAccKey, err := asserts.Decode([]byte(encodedTestStoreAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode test store assertion: %v", err)) + } + + TestRootAccount = acct + TestRootAccountKey = accKey + TestStoreAccountKey = storeAccKey + Trusted = []asserts.Assertion{TestRootAccount, TestRootAccountKey} +} diff --git a/asserts/user.go b/asserts/user.go new file mode 100644 index 00000000..25e66487 --- /dev/null +++ b/asserts/user.go @@ -0,0 +1,263 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts + +import ( + "fmt" + "net/mail" + "regexp" + "strconv" + "strings" + "time" +) + +var validSystemUserUsernames = regexp.MustCompile(`^[a-z0-9][-a-z0-9+.-_]*$`) + +// SystemUser holds a system-user assertion which allows creating local +// system users. +type SystemUser struct { + assertionBase + series []string + models []string + sshKeys []string + since time.Time + until time.Time +} + +// BrandID returns the brand identifier that signed this assertion. +func (su *SystemUser) BrandID() string { + return su.HeaderString("brand-id") +} + +// Email returns the email address that this assertion is valid for. +func (su *SystemUser) Email() string { + return su.HeaderString("email") +} + +// Series returns the series that this assertion is valid for. +func (su *SystemUser) Series() []string { + return su.series +} + +// Models returns the models that this assertion is valid for. +func (su *SystemUser) Models() []string { + return su.models +} + +// Name returns the full name of the user (e.g. Random Guy). +func (su *SystemUser) Name() string { + return su.HeaderString("name") +} + +// Username returns the system user name that should be created (e.g. "foo"). +func (su *SystemUser) Username() string { + return su.HeaderString("username") +} + +// Password returns the crypt(3) compatible password for the user. +// Note that only ID: $6$ or stronger is supported (sha512crypt). +func (su *SystemUser) Password() string { + return su.HeaderString("password") +} + +// SSHKeys returns the ssh keys for the user. +func (su *SystemUser) SSHKeys() []string { + return su.sshKeys +} + +// Since returns the time since the assertion is valid. +func (su *SystemUser) Since() time.Time { + return su.since +} + +// Until returns the time until the assertion is valid. +func (su *SystemUser) Until() time.Time { + return su.until +} + +// ValidAt returns whether the system-user is valid at 'when' time. +func (su *SystemUser) ValidAt(when time.Time) bool { + valid := when.After(su.since) || when.Equal(su.since) + if valid { + valid = when.Before(su.until) + } + return valid +} + +// Implement further consistency checks. +func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error { + // Do the cross-checks when this assertion is actually used, + // i.e. in the create-user code. See also Model.checkConsitency + + return nil +} + +// sanity +var _ consistencyChecker = (*SystemUser)(nil) + +type shadow struct { + ID string + Rounds string + Salt string + Hash string +} + +// crypt(3) compatible hashes have the forms: +// - $id$salt$hash +// - $id$rounds=N$salt$hash +func parseShadowLine(line string) (*shadow, error) { + l := strings.SplitN(line, "$", 5) + if len(l) != 4 && len(l) != 5 { + return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`) + } + + // if rounds is the second field, the line must consist of 4 + if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 { + return nil, fmt.Errorf(`missing hash field`) + } + + // shadow line without $rounds=N$ + if len(l) == 4 { + return &shadow{ + ID: l[1], + Salt: l[2], + Hash: l[3], + }, nil + } + // shadow line with rounds + return &shadow{ + ID: l[1], + Rounds: l[2], + Salt: l[3], + Hash: l[4], + }, nil +} + +func checkHashedPassword(headers map[string]interface{}, name string) (string, error) { + pw, err := checkOptionalString(headers, name) + if err != nil { + return "", err + } + // the pw string is optional, so just return if its empty + if pw == "" { + return "", nil + } + + // parse the shadow line + shd, err := parseShadowLine(pw) + if err != nil { + return "", fmt.Errorf(`%q header invalid: %s`, name, err) + } + + // and verify it + + // see crypt(3), ID 6 means SHA-512 (since glibc 2.7) + ID, err := strconv.Atoi(shd.ID) + if err != nil { + return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID) + } + // double check that we only allow modern hashes + if ID < 6 { + return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name) + } + + // the $rounds=N$ part is optional + if strings.HasPrefix(shd.Rounds, "rounds=") { + rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1]) + if err != nil { + return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err) + } + if rounds < 5000 || rounds > 999999999 { + return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds) + } + } + + // see crypt(3) for the legal chars + validSaltAndHash := regexp.MustCompile(`^[a-zA-Z0-9./]+$`) + if !validSaltAndHash.MatchString(shd.Salt) { + return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt) + } + if !validSaltAndHash.MatchString(shd.Hash) { + return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash) + } + + return pw, nil +} + +func assembleSystemUser(assert assertionBase) (Assertion, error) { + // brand-id here can be different from authority-id, + // the code using the assertion must use the policy set + // by the model assertion system-user-authority header + email, err := checkNotEmptyString(assert.headers, "email") + if err != nil { + return nil, err + } + if _, err := mail.ParseAddress(email); err != nil { + return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err) + } + + series, err := checkStringList(assert.headers, "series") + if err != nil { + return nil, err + } + models, err := checkStringList(assert.headers, "models") + if err != nil { + return nil, err + } + if _, err := checkOptionalString(assert.headers, "name"); err != nil { + return nil, err + } + if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil { + return nil, err + } + if _, err := checkHashedPassword(assert.headers, "password"); err != nil { + return nil, err + } + + sshKeys, err := checkStringList(assert.headers, "ssh-keys") + if err != nil { + return nil, err + } + since, err := checkRFC3339Date(assert.headers, "since") + if err != nil { + return nil, err + } + until, err := checkRFC3339Date(assert.headers, "until") + if err != nil { + return nil, err + } + if until.Before(since) { + return nil, fmt.Errorf("'until' time cannot be before 'since' time") + } + + // "global" system-user assertion can only be valid for 1y + if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) { + return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified") + } + + return &SystemUser{ + assertionBase: assert, + series: series, + models: models, + sshKeys: sshKeys, + since: since, + until: until, + }, nil +} diff --git a/asserts/user_test.go b/asserts/user_test.go new file mode 100644 index 00000000..a2d86246 --- /dev/null +++ b/asserts/user_test.go @@ -0,0 +1,200 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package asserts_test + +import ( + "fmt" + "strings" + "time" + + "github.com/snapcore/snapd/asserts" + . "gopkg.in/check.v1" +) + +var ( + _ = Suite(&systemUserSuite{}) +) + +type systemUserSuite struct { + until time.Time + untilLine string + since time.Time + sinceLine string + + modelsLine string + + systemUserStr string +} + +const systemUserExample = "type: system-user\n" + + "authority-id: canonical\n" + + "brand-id: canonical\n" + + "email: foo@example.com\n" + + "series:\n" + + " - 16\n" + + "MODELSLINE\n" + + "name: Nice Guy\n" + + "username: guy\n" + + "password: $6$salt$hash\n" + + "ssh-keys:\n" + + " - ssh-rsa AAAABcdefg\n" + + "SINCELINE\n" + + "UNTILLINE\n" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + +func (s *systemUserSuite) SetUpTest(c *C) { + s.since = time.Now().Truncate(time.Second) + s.sinceLine = fmt.Sprintf("since: %s\n", s.since.Format(time.RFC3339)) + s.until = time.Now().AddDate(0, 1, 0).Truncate(time.Second) + s.untilLine = fmt.Sprintf("until: %s\n", s.until.Format(time.RFC3339)) + s.modelsLine = "models:\n - frobinator\n" + s.systemUserStr = strings.Replace(systemUserExample, "UNTILLINE\n", s.untilLine, 1) + s.systemUserStr = strings.Replace(s.systemUserStr, "SINCELINE\n", s.sinceLine, 1) + s.systemUserStr = strings.Replace(s.systemUserStr, "MODELSLINE\n", s.modelsLine, 1) +} + +func (s *systemUserSuite) TestDecodeOK(c *C) { + a, err := asserts.Decode([]byte(s.systemUserStr)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SystemUserType) + systemUser := a.(*asserts.SystemUser) + c.Check(systemUser.BrandID(), Equals, "canonical") + c.Check(systemUser.Email(), Equals, "foo@example.com") + c.Check(systemUser.Series(), DeepEquals, []string{"16"}) + c.Check(systemUser.Models(), DeepEquals, []string{"frobinator"}) + c.Check(systemUser.Name(), Equals, "Nice Guy") + c.Check(systemUser.Username(), Equals, "guy") + c.Check(systemUser.Password(), Equals, "$6$salt$hash") + c.Check(systemUser.SSHKeys(), DeepEquals, []string{"ssh-rsa AAAABcdefg"}) + c.Check(systemUser.Since().Equal(s.since), Equals, true) + c.Check(systemUser.Until().Equal(s.until), Equals, true) +} + +func (s *systemUserSuite) TestDecodePasswd(c *C) { + validTests := []struct{ original, valid string }{ + {"password: $6$salt$hash\n", "password: $6$rounds=9999$salt$hash\n"}, + {"password: $6$salt$hash\n", ""}, + } + for _, test := range validTests { + valid := strings.Replace(s.systemUserStr, test.original, test.valid, 1) + _, err := asserts.Decode([]byte(valid)) + c.Check(err, IsNil) + } +} + +func (s *systemUserSuite) TestValidAt(c *C) { + a, err := asserts.Decode([]byte(s.systemUserStr)) + c.Assert(err, IsNil) + su := a.(*asserts.SystemUser) + + c.Check(su.ValidAt(su.Since()), Equals, true) + c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false) + c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, true) + + c.Check(su.ValidAt(su.Until()), Equals, false) + c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, true) + c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false) +} + +func (s *systemUserSuite) TestValidAtRevoked(c *C) { + // With since == until, i.e. system-user has been revoked. + revoked := strings.Replace(s.systemUserStr, s.sinceLine, fmt.Sprintf("since: %s\n", s.until.Format(time.RFC3339)), 1) + a, err := asserts.Decode([]byte(revoked)) + c.Assert(err, IsNil) + su := a.(*asserts.SystemUser) + + c.Check(su.ValidAt(su.Since()), Equals, false) + c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false) + c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, false) + + c.Check(su.ValidAt(su.Until()), Equals, false) + c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, false) + c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false) +} + +const ( + systemUserErrPrefix = "assertion system-user: " +) + +func (s *systemUserSuite) TestDecodeInvalid(c *C) { + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"brand-id: canonical\n", "", `"brand-id" header is mandatory`}, + {"brand-id: canonical\n", "brand-id: \n", `"brand-id" header should not be empty`}, + {"email: foo@example.com\n", "", `"email" header is mandatory`}, + {"email: foo@example.com\n", "email: \n", `"email" header should not be empty`}, + {"email: foo@example.com\n", "email: \n", `"email" header must be a RFC 5322 compliant email address: mail: missing @ in addr-spec`}, + {"email: foo@example.com\n", "email: no-mail\n", `"email" header must be a RFC 5322 compliant email address:.*`}, + {"series:\n - 16\n", "series: \n", `"series" header must be a list of strings`}, + {"series:\n - 16\n", "series: something\n", `"series" header must be a list of strings`}, + {"models:\n - frobinator\n", "models: \n", `"models" header must be a list of strings`}, + {"models:\n - frobinator\n", "models: something\n", `"models" header must be a list of strings`}, + {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: \n", `"ssh-keys" header must be a list of strings`}, + {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: something\n", `"ssh-keys" header must be a list of strings`}, + {"name: Nice Guy\n", "name:\n - foo\n", `"name" header must be a string`}, + {"username: guy\n", "username:\n - foo\n", `"username" header must be a string`}, + {"username: guy\n", "username: bäää\n", `"username" header contains invalid characters: "bäää"`}, + {"username: guy\n", "", `"username" header is mandatory`}, + {"password: $6$salt$hash\n", "password:\n - foo\n", `"password" header must be a string`}, + {"password: $6$salt$hash\n", "password: cleartext\n", `"password" header invalid: hashed password must be of the form "\$integer-id\$salt\$hash", see crypt\(3\)`}, + {"password: $6$salt$hash\n", "password: $ni!$salt$hash\n", `"password" header must start with "\$integer-id\$", got "ni!"`}, + {"password: $6$salt$hash\n", "password: $3$salt$hash\n", `"password" header only supports \$id\$ values of 6 \(sha512crypt\) or higher`}, + {"password: $6$salt$hash\n", "password: $7$invalid-salt$hash\n", `"password" header has invalid chars in salt "invalid-salt"`}, + {"password: $6$salt$hash\n", "password: $8$salt$invalid-hash\n", `"password" header has invalid chars in hash "invalid-hash"`}, + {"password: $6$salt$hash\n", "password: $8$rounds=9999$hash\n", `"password" header invalid: missing hash field`}, + {"password: $6$salt$hash\n", "password: $8$rounds=xxx$salt$hash\n", `"password" header has invalid number of rounds:.*`}, + {"password: $6$salt$hash\n", "password: $8$rounds=1$salt$hash\n", `"password" header rounds parameter out of bounds: 1`}, + {"password: $6$salt$hash\n", "password: $8$rounds=1999999999$salt$hash\n", `"password" header rounds parameter out of bounds: 1999999999`}, + {s.sinceLine, "since: \n", `"since" header should not be empty`}, + {s.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`}, + {s.untilLine, "until: \n", `"until" header should not be empty`}, + {s.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`}, + {s.untilLine, "until: 1002-11-01T22:08:41+00:00\n", `'until' time cannot be before 'since' time`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(s.systemUserStr, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, systemUserErrPrefix+test.expectedErr) + } +} + +func (s *systemUserSuite) TestUntilNoModels(c *C) { + // no models is good for <1y + su := strings.Replace(s.systemUserStr, s.modelsLine, "", -1) + _, err := asserts.Decode([]byte(su)) + c.Check(err, IsNil) + + // but invalid for more than one year + oneYearPlusOne := time.Now().AddDate(1, 0, 1).Truncate(time.Second) + su = strings.Replace(su, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1) + _, err = asserts.Decode([]byte(su)) + c.Check(err, ErrorMatches, systemUserErrPrefix+"'until' time cannot be more than 365 days in the future when no models are specified") +} + +func (s *systemUserSuite) TestUntilWithModels(c *C) { + // with models it can be valid forever + oneYearPlusOne := time.Now().AddDate(10, 0, 1).Truncate(time.Second) + su := strings.Replace(s.systemUserStr, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1) + _, err := asserts.Decode([]byte(su)) + c.Check(err, IsNil) +} diff --git a/boot/boottest/mockbootloader.go b/boot/boottest/mockbootloader.go new file mode 100644 index 00000000..47de7ee0 --- /dev/null +++ b/boot/boottest/mockbootloader.go @@ -0,0 +1,72 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package boottest + +import ( + "path/filepath" +) + +// MockBootloader mocks the bootloader interface and records all +// set/get calls. +type MockBootloader struct { + BootVars map[string]string + SetErr error + GetErr error + + name string + bootdir string +} + +func NewMockBootloader(name, bootdir string) *MockBootloader { + return &MockBootloader{ + name: name, + bootdir: bootdir, + + BootVars: make(map[string]string), + } +} + +func (b *MockBootloader) SetBootVars(values map[string]string) error { + for k, v := range values { + b.BootVars[k] = v + } + return b.SetErr +} + +func (b *MockBootloader) GetBootVars(keys ...string) (map[string]string, error) { + out := map[string]string{} + for _, k := range keys { + out[k] = b.BootVars[k] + } + + return out, b.GetErr +} + +func (b *MockBootloader) Dir() string { + return b.bootdir +} + +func (b *MockBootloader) Name() string { + return b.name +} + +func (b *MockBootloader) ConfigFile() string { + return filepath.Join(b.bootdir, "mockboot/mockboot.cfg") +} diff --git a/boot/kernel_os.go b/boot/kernel_os.go new file mode 100644 index 00000000..52852e4a --- /dev/null +++ b/boot/kernel_os.go @@ -0,0 +1,204 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package boot + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/partition" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" +) + +// RemoveKernelAssets removes the unpacked kernel/initrd for the given +// kernel snap. +func RemoveKernelAssets(s snap.PlaceInfo) error { + bootloader, err := partition.FindBootloader() + if err != nil { + return fmt.Errorf("no not remove kernel assets: %s", err) + } + + // remove the kernel blob + blobName := filepath.Base(s.MountFile()) + dstDir := filepath.Join(bootloader.Dir(), blobName) + if err := os.RemoveAll(dstDir); err != nil { + return err + } + + return nil +} + +func copyAll(src, dst string) error { + if output, err := exec.Command("cp", "-aLv", src, dst).CombinedOutput(); err != nil { + return fmt.Errorf("cannot copy %q -> %q: %s (%s)", src, dst, err, output) + } + return nil +} + +// ExtractKernelAssets extracts kernel/initrd/dtb data from the given +// kernel snap, if required, to a versioned bootloader directory so +// that the bootloader can use it. +func ExtractKernelAssets(s *snap.Info, snapf snap.Container) error { + if s.Type != snap.TypeKernel { + return fmt.Errorf("cannot extract kernel assets from snap type %q", s.Type) + } + + bootloader, err := partition.FindBootloader() + if err != nil { + return fmt.Errorf("cannot extract kernel assets: %s", err) + } + + if bootloader.Name() == "grub" { + return nil + } + + // now do the kernel specific bits + blobName := filepath.Base(s.MountFile()) + dstDir := filepath.Join(bootloader.Dir(), blobName) + if err := os.MkdirAll(dstDir, 0755); err != nil { + return err + } + dir, err := os.Open(dstDir) + if err != nil { + return err + } + defer dir.Close() + + for _, src := range []string{"kernel.img", "initrd.img"} { + if err := snapf.Unpack(src, dstDir); err != nil { + return err + } + if err := dir.Sync(); err != nil { + return err + } + } + if err := snapf.Unpack("dtbs/*", dstDir); err != nil { + return err + } + + return dir.Sync() +} + +// SetNextBoot will schedule the given OS or kernel snap to be used in +// the next boot +func SetNextBoot(s *snap.Info) error { + if release.OnClassic { + return nil + } + if s.Type != snap.TypeOS && s.Type != snap.TypeKernel { + return nil + } + + bootloader, err := partition.FindBootloader() + if err != nil { + return fmt.Errorf("cannot set next boot: %s", err) + } + + var nextBoot, goodBoot string + switch s.Type { + case snap.TypeOS: + nextBoot = "snap_try_core" + goodBoot = "snap_core" + case snap.TypeKernel: + nextBoot = "snap_try_kernel" + goodBoot = "snap_kernel" + } + blobName := filepath.Base(s.MountFile()) + + // check if we actually need to do anything, i.e. the exact same + // kernel/core revision got installed again (e.g. firstboot) + m, err := bootloader.GetBootVars(goodBoot) + if err != nil { + return err + } + if m[goodBoot] == blobName { + return nil + } + + return bootloader.SetBootVars(map[string]string{ + nextBoot: blobName, + "snap_mode": "try", + }) +} + +// KernelOrOsRebootRequired returns whether a reboot is required to swith to the given OS or kernel snap. +func KernelOrOsRebootRequired(s *snap.Info) bool { + if s.Type != snap.TypeKernel && s.Type != snap.TypeOS { + return false + } + + bootloader, err := partition.FindBootloader() + if err != nil { + logger.Noticef("cannot get boot settings: %s", err) + return false + } + + var nextBoot, goodBoot string + switch s.Type { + case snap.TypeKernel: + nextBoot = "snap_try_kernel" + goodBoot = "snap_kernel" + case snap.TypeOS: + nextBoot = "snap_try_core" + goodBoot = "snap_core" + } + + m, err := bootloader.GetBootVars(nextBoot, goodBoot) + if err != nil { + logger.Noticef("cannot get boot variables: %s", err) + return false + } + + squashfsName := filepath.Base(s.MountFile()) + if m[nextBoot] == squashfsName && m[goodBoot] != m[nextBoot] { + return true + } + + return false +} + +// InUse checks if the given name/revision is used in the +// boot environment +func InUse(name string, rev snap.Revision) bool { + bootloader, err := partition.FindBootloader() + if err != nil { + logger.Noticef("cannot get boot settings: %s", err) + return false + } + + bootVars, err := bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_core", "snap_try_core") + if err != nil { + logger.Noticef("cannot get boot vars: %s", err) + return false + } + + snapFile := filepath.Base(snap.MountFile(name, rev)) + for _, bootVar := range bootVars { + if bootVar == snapFile { + return true + } + } + + return false +} diff --git a/boot/kernel_os_test.go b/boot/kernel_os_test.go new file mode 100644 index 00000000..d37ca865 --- /dev/null +++ b/boot/kernel_os_test.go @@ -0,0 +1,251 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package boot_test + +import ( + "io/ioutil" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/partition" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" +) + +func TestBoot(t *testing.T) { TestingT(t) } + +type kernelOSSuite struct { + bootloader *boottest.MockBootloader +} + +var _ = Suite(&kernelOSSuite{}) + +func (s *kernelOSSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + s.bootloader = boottest.NewMockBootloader("mock", c.MkDir()) + partition.ForceBootloader(s.bootloader) +} + +func (s *kernelOSSuite) TearDownTest(c *C) { + dirs.SetRootDir("") + partition.ForceBootloader(nil) +} + +const packageKernel = ` +name: ubuntu-kernel +version: 4.0-1 +type: kernel +vendor: Someone +` + +func (s *kernelOSSuite) TestExtractKernelAssetsAndRemove(c *C) { + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"dtbs/foo.dtb", "g'day, I'm foo.dtb"}, + {"dtbs/bar.dtb", "hello, I'm bar.dtb"}, + // must be last + {"meta/kernel.yaml", "version: 4.2"}, + } + + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // this is where the kernel/initrd is unpacked + bootdir := s.bootloader.Dir() + + kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap") + + for _, def := range files { + if def[0] == "meta/kernel.yaml" { + break + } + + fullFn := filepath.Join(kernelAssetsDir, def[0]) + content, err := ioutil.ReadFile(fullFn) + c.Assert(err, IsNil) + c.Assert(string(content), Equals, def[1]) + } + + // remove + err = boot.RemoveKernelAssets(info) + c.Assert(err, IsNil) + + c.Check(osutil.FileExists(kernelAssetsDir), Equals, false) +} + +func (s *kernelOSSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) { + // pretend to be a grub system + mockGrub := boottest.NewMockBootloader("grub", c.MkDir()) + partition.ForceBootloader(mockGrub) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snap.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = boot.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is *not* here + kernimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, false) +} + +func (s *kernelOSSuite) TestExtractKernelAssetsError(c *C) { + info := &snap.Info{} + info.Type = snap.TypeApp + + err := boot.ExtractKernelAssets(info, nil) + c.Assert(err, ErrorMatches, `cannot extract kernel assets from snap type "app"`) +} + +// SetNextBoot should do nothing on classic LP: #1580403 +func (s *kernelOSSuite) TestSetNextBootOnClassic(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + // Create a fake OS snap that we try to update + snapInfo := snaptest.MockSnap(c, "name: os\ntype: os", "SNAP", &snap.SideInfo{Revision: snap.R(42)}) + err := boot.SetNextBoot(snapInfo) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, HasLen, 0) +} + +func (s *kernelOSSuite) TestSetNextBootForCore(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeOS + info.RealName = "core" + info.Revision = snap.R(100) + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_try_core": "core_100.snap", + "snap_mode": "try", + }) + + c.Check(boot.KernelOrOsRebootRequired(info), Equals, true) +} + +func (s *kernelOSSuite) TestSetNextBootForKernel(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeKernel + info.RealName = "krnl" + info.Revision = snap.R(42) + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_try_kernel": "krnl_42.snap", + "snap_mode": "try", + }) + + s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" + s.bootloader.BootVars["snap_try_kernel"] = "krnl_42.snap" + c.Check(boot.KernelOrOsRebootRequired(info), Equals, true) + + // simulate good boot + s.bootloader.BootVars["snap_kernel"] = "krnl_42.snap" + c.Check(boot.KernelOrOsRebootRequired(info), Equals, false) +} + +func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernel(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + info := &snap.Info{} + info.Type = snap.TypeKernel + info.RealName = "krnl" + info.Revision = snap.R(40) + + s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" + + err := boot.SetNextBoot(info) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snap_kernel": "krnl_40.snap", + }) +} + +func (s *kernelOSSuite) TestInUse(c *C) { + for _, t := range []struct { + bootVarKey string + bootVarValue string + + snapName string + snapRev snap.Revision + + inUse bool + }{ + // in use + {"snap_kernel", "kernel_41.snap", "kernel", snap.R(41), true}, + {"snap_try_kernel", "kernel_82.snap", "kernel", snap.R(82), true}, + {"snap_core", "core_21.snap", "core", snap.R(21), true}, + {"snap_try_core", "core_42.snap", "core", snap.R(42), true}, + // not in use + {"snap_core", "core_111.snap", "core", snap.R(21), false}, + {"snap_try_core", "core_111.snap", "core", snap.R(21), false}, + {"snap_kernel", "kernel_111.snap", "kernel", snap.R(1), false}, + {"snap_try_kernel", "kernel_111.snap", "kernel", snap.R(1), false}, + } { + s.bootloader.BootVars[t.bootVarKey] = t.bootVarValue + c.Assert(boot.InUse(t.snapName, t.snapRev), Equals, t.inUse, Commentf("unexpected result: %s %s %v", t.snapName, t.snapRev, t.inUse)) + } +} diff --git a/client/aliases.go b/client/aliases.go new file mode 100644 index 00000000..bf45f4cf --- /dev/null +++ b/client/aliases.go @@ -0,0 +1,102 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" +) + +// aliasAction represents an action performed on aliases. +// With action "unalias" if Snap and Alias are set to the same value, +// snapd will check if what is referred to is indeed a snap or an alias. +type aliasAction struct { + Action string `json:"action"` + Snap string `json:"snap,omitempty"` + App string `json:"app,omitempty"` + Alias string `json:"alias,omitempty"` +} + +// performAliasAction performs a single action on aliases. +func (client *Client) performAliasAction(sa *aliasAction) (changeID string, err error) { + b, err := json.Marshal(sa) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/aliases", nil, nil, bytes.NewReader(b)) +} + +// Alias sets up a manual alias from alias to app in snapName. +func (client *Client) Alias(snapName, app, alias string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "alias", + Snap: snapName, + App: app, + Alias: alias, + }) +} + +// // DisableAllAliases disables all aliases of a snap, removing all manual ones. +func (client *Client) DisableAllAliases(snapName string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "unalias", + Snap: snapName, + }) +} + +// RemoveManualAlias removes a manual alias. +func (client *Client) RemoveManualAlias(alias string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "unalias", + Alias: alias, + }) +} + +// Unalias tears down a manual alias or disables all aliases of a snap (removing all manual ones) +func (client *Client) Unalias(aliasOrSnap string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "unalias", + Snap: aliasOrSnap, + Alias: aliasOrSnap, + }) +} + +// Prefer enables all aliases of a snap in preference to conflicting aliases +// of other snaps whose aliases will be disabled (removed for manual ones). +func (client *Client) Prefer(snapName string) (changeID string, err error) { + return client.performAliasAction(&aliasAction{ + Action: "prefer", + Snap: snapName, + }) +} + +// AliasStatus represents the status of an alias. +type AliasStatus struct { + Command string `json:"command"` + Status string `json:"status"` + Manual string `json:"manual,omitempty"` + Auto string `json:"auto,omitempty"` +} + +// Aliases returns a map snap -> alias -> AliasStatus for all snaps and aliases in the system. +func (client *Client) Aliases() (allStatuses map[string]map[string]AliasStatus, err error) { + _, err = client.doSync("GET", "/v2/aliases", nil, nil, nil, &allStatuses) + return +} diff --git a/client/aliases_test.go b/client/aliases_test.go new file mode 100644 index 00000000..a850e529 --- /dev/null +++ b/client/aliases_test.go @@ -0,0 +1,195 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "encoding/json" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) TestClientAliasCallsEndpoint(c *check.C) { + cs.cli.Alias("alias-snap", "cmd1", "alias1") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientAlias(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.Alias("alias-snap", "cmd1", "alias1") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "chgid") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "alias", + "snap": "alias-snap", + "app": "cmd1", + "alias": "alias1", + }) +} + +func (cs *clientSuite) TestClientUnaliasCallsEndpoint(c *check.C) { + cs.cli.Unalias("alias1") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientUnalias(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.Unalias("alias1") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "chgid") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "unalias", + "snap": "alias1", + "alias": "alias1", + }) +} + +func (cs *clientSuite) TestClientDisableAllAliasesCallsEndpoint(c *check.C) { + cs.cli.DisableAllAliases("some-snap") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientDisableAllAliases(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.DisableAllAliases("some-snap") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "chgid") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "unalias", + "snap": "some-snap", + }) +} + +func (cs *clientSuite) TestClientRemoveManualAliasCallsEndpoint(c *check.C) { + cs.cli.RemoveManualAlias("alias1") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientRemoveManualAlias(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.RemoveManualAlias("alias1") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "chgid") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "unalias", + "alias": "alias1", + }) +} + +func (cs *clientSuite) TestClientPreferCallsEndpoint(c *check.C) { + cs.cli.Prefer("some-snap") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientPrefer(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "chgid" + }` + id, err := cs.cli.Prefer("some-snap") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "chgid") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "prefer", + "snap": "some-snap", + }) +} + +func (cs *clientSuite) TestClientAliasesCallsEndpoint(c *check.C) { + _, _ = cs.cli.Aliases() + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/aliases") +} + +func (cs *clientSuite) TestClientAliases(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": { + "foo": { + "foo0": {"command": "foo", "status": "auto", "auto": "foo"}, + "foo_reset": {"command": "foo.reset", "manual": "reset", "status": "manual"} + }, + "bar": { + "bar_dump": {"command": "bar.dump", "status": "manual", "manual": "dump"}, + "bar_dump.1": {"command": "bar.dump", "status": "disabled", "auto": "dump"} + } + } + }` + allStatuses, err := cs.cli.Aliases() + c.Assert(err, check.IsNil) + c.Check(allStatuses, check.DeepEquals, map[string]map[string]client.AliasStatus{ + "foo": { + "foo0": {Command: "foo", Status: "auto", Auto: "foo"}, + "foo_reset": {Command: "foo.reset", Status: "manual", Manual: "reset"}, + }, + "bar": { + "bar_dump": {Command: "bar.dump", Status: "manual", Manual: "dump"}, + "bar_dump.1": {Command: "bar.dump", Status: "disabled", Auto: "dump"}, + }, + }) +} diff --git a/client/apps.go b/client/apps.go new file mode 100644 index 00000000..adebbd95 --- /dev/null +++ b/client/apps.go @@ -0,0 +1,242 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + "strings" + "time" +) + +// AppInfo describes a single snap application. +type AppInfo struct { + Snap string `json:"snap,omitempty"` + Name string `json:"name"` + DesktopFile string `json:"desktop-file,omitempty"` + Daemon string `json:"daemon,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Active bool `json:"active,omitempty"` +} + +// IsService returns true if the application is a background daemon. +func (a *AppInfo) IsService() bool { + if a == nil { + return false + } + if a.Daemon == "" { + return false + } + + return true +} + +// AppOptions represent the options of the Apps call. +type AppOptions struct { + // If Service is true, only return apps that are services + // (app.IsService() is true); otherwise, return all. + Service bool +} + +// Apps returns information about all matching apps. Each name can be +// either a snap or a snap.app. If names is empty, list all (that +// satisfy opts). +func (client *Client) Apps(names []string, opts AppOptions) ([]*AppInfo, error) { + q := make(url.Values) + if len(names) > 0 { + q.Add("names", strings.Join(names, ",")) + } + if opts.Service { + q.Add("select", "service") + } + + var appInfos []*AppInfo + _, err := client.doSync("GET", "/v2/apps", q, nil, nil, &appInfos) + + return appInfos, err +} + +// LogOptions represent the options of the Logs call. +type LogOptions struct { + N int // The maximum number of log lines to retrieve initially. If <0, no limit. + Follow bool // Whether to continue returning new lines as they appear +} + +// A Log holds the information of a single syslog entry +type Log struct { + Timestamp time.Time `json:"timestamp"` // Timestamp of the event, in RFC3339 format to µs precision. + Message string `json:"message"` // The log message itself + SID string `json:"sid"` // The syslog identifier + PID string `json:"pid"` // The process identifier +} + +func (l Log) String() string { + return fmt.Sprintf("%s %s[%s]: %s", l.Timestamp.Format(time.RFC3339), l.SID, l.PID, l.Message) +} + +// Logs asks for the logs of a series of services, by name. +func (client *Client) Logs(names []string, opts LogOptions) (<-chan Log, error) { + query := url.Values{} + if len(names) > 0 { + query.Set("names", strings.Join(names, ",")) + } + query.Set("n", strconv.Itoa(opts.N)) + if opts.Follow { + query.Set("follow", strconv.FormatBool(opts.Follow)) + } + + rsp, err := client.raw("GET", "/v2/logs", query, nil, nil) + if err != nil { + return nil, err + } + + ch := make(chan Log, 20) + go func() { + // logs come in application/json-seq, described in RFC7464: it's + // a series of . Decoders are + // expected to skip invalid or truncated or empty records. + scanner := bufio.NewScanner(rsp.Body) + for scanner.Scan() { + buf := scanner.Bytes() // the scanner prunes the ending LF + if len(buf) < 1 { + // truncated record? skip + continue + } + idx := bytes.IndexByte(buf, 0x1E) // find the initial RS + if idx < 0 { + // no RS? skip + continue + } + buf = buf[idx+1:] // drop the initial RS + var log Log + if err := json.Unmarshal(buf, &log); err != nil { + // truncated/corrupted/binary record? skip + continue + } + ch <- log + } + close(ch) + rsp.Body.Close() + }() + + return ch, nil +} + +// ErrNoNames is returned by Start, Stop, or Restart, when the given +// list of things on which to operate is empty. +var ErrNoNames = errors.New(`"names" must not be empty`) + +type appInstruction struct { + Action string `json:"action"` + Names []string `json:"names"` + StartOptions + StopOptions + RestartOptions +} + +// StartOptions represent the different options of the Start call. +type StartOptions struct { + // Enable, as well as starting, the listed services. A + // disabled service does not start on boot. + Enable bool `json:"enable,omitempty"` +} + +// Start services. +// +// It takes a list of names that can be snaps, of which all their +// services are started, or snap.service which are individual +// services to start; it shouldn't be empty. +func (client *Client) Start(names []string, opts StartOptions) (changeID string, err error) { + if len(names) == 0 { + return "", ErrNoNames + } + + buf, err := json.Marshal(appInstruction{ + Action: "start", + Names: names, + StartOptions: opts, + }) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf)) +} + +// StopOptions represent the different options of the Stop call. +type StopOptions struct { + // Disable, as well as stopping, the listed services. A + // service that is not disabled starts on boot. + Disable bool `json:"disable,omitempty"` +} + +// Stop services. +// +// It takes a list of names that can be snaps, of which all their +// services are stopped, or snap.service which are individual +// services to stop; it shouldn't be empty. +func (client *Client) Stop(names []string, opts StopOptions) (changeID string, err error) { + if len(names) == 0 { + return "", ErrNoNames + } + + buf, err := json.Marshal(appInstruction{ + Action: "stop", + Names: names, + StopOptions: opts, + }) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf)) +} + +// RestartOptions represent the different options of the Restart call. +type RestartOptions struct { + // Reload the services, if possible (i.e. if the App has a + // ReloadCommand, invoque it), instead of restarting. + Reload bool `json:"reload,omitempty"` +} + +// Restart services. +// +// It takes a list of names that can be snaps, of which all their +// services are restarted, or snap.service which are individual +// services to restart; it shouldn't be empty. If the service is not +// running, starts it. +func (client *Client) Restart(names []string, opts RestartOptions) (changeID string, err error) { + if len(names) == 0 { + return "", ErrNoNames + } + + buf, err := json.Marshal(appInstruction{ + Action: "restart", + Names: names, + RestartOptions: opts, + }) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf)) +} diff --git a/client/apps_test.go b/client/apps_test.go new file mode 100644 index 00000000..deecb38d --- /dev/null +++ b/client/apps_test.go @@ -0,0 +1,372 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func mksvc(snap, app string) *client.AppInfo { + return &client.AppInfo{ + Snap: snap, + Name: app, + Daemon: "simple", + Active: true, + Enabled: true, + } + +} + +func testClientApps(cs *clientSuite, c *check.C) ([]*client.AppInfo, error) { + services, err := cs.cli.Apps([]string{"foo", "bar"}, client.AppOptions{}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps") + c.Check(cs.req.Method, check.Equals, "GET") + query := cs.req.URL.Query() + c.Check(query, check.HasLen, 1) + c.Check(query.Get("names"), check.Equals, "foo,bar") + + return services, err +} + +func testClientAppsService(cs *clientSuite, c *check.C) ([]*client.AppInfo, error) { + services, err := cs.cli.Apps([]string{"foo", "bar"}, client.AppOptions{Service: true}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps") + c.Check(cs.req.Method, check.Equals, "GET") + query := cs.req.URL.Query() + c.Check(query, check.HasLen, 2) + c.Check(query.Get("names"), check.Equals, "foo,bar") + c.Check(query.Get("select"), check.Equals, "service") + + return services, err +} + +var appcheckers = []func(*clientSuite, *check.C) ([]*client.AppInfo, error){testClientApps, testClientAppsService} + +func (cs *clientSuite) TestClientServiceGetHappy(c *check.C) { + expected := []*client.AppInfo{mksvc("foo", "foo"), mksvc("bar", "bar1")} + buf, err := json.Marshal(expected) + c.Assert(err, check.IsNil) + cs.rsp = fmt.Sprintf(`{"type": "sync", "result": %s}`, buf) + for _, chkr := range appcheckers { + actual, err := chkr(cs, c) + c.Assert(err, check.IsNil) + c.Check(actual, check.DeepEquals, expected) + } +} + +func (cs *clientSuite) TestClientServiceGetSad(c *check.C) { + cs.err = fmt.Errorf("xyzzy") + for _, chkr := range appcheckers { + actual, err := chkr(cs, c) + c.Assert(err, check.ErrorMatches, ".* xyzzy") + c.Check(actual, check.HasLen, 0) + } +} + +func testClientLogs(cs *clientSuite, c *check.C) ([]client.Log, error) { + ch, err := cs.cli.Logs([]string{"foo", "bar"}, client.LogOptions{N: -1, Follow: false}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/logs") + c.Check(cs.req.Method, check.Equals, "GET") + query := cs.req.URL.Query() + c.Check(query, check.HasLen, 2) + c.Check(query.Get("names"), check.Equals, "foo,bar") + c.Check(query.Get("n"), check.Equals, "-1") + + var logs []client.Log + if ch != nil { + for log := range ch { + logs = append(logs, log) + } + } + + return logs, err +} + +func (cs *clientSuite) TestClientLogsHappy(c *check.C) { + cs.rsp = ` +{"message":"hello"} +{"message":"bye"} +`[1:] // remove the first \n + + logs, err := testClientLogs(cs, c) + c.Assert(err, check.IsNil) + c.Check(logs, check.DeepEquals, []client.Log{{Message: "hello"}, {Message: "bye"}}) +} + +func (cs *clientSuite) TestClientLogsDealsWithIt(c *check.C) { + cs.rsp = `this is a line with no RS on it +this is a line with a RS after some junk{"message": "hello"} +{"message": "bye"} +and that was a regular line. The next one is empty, despite having a RS (and the one after is entirely empty): + + +` + logs, err := testClientLogs(cs, c) + c.Assert(err, check.IsNil) + c.Check(logs, check.DeepEquals, []client.Log{{Message: "hello"}, {Message: "bye"}}) +} + +func (cs *clientSuite) TestClientLogsSad(c *check.C) { + cs.err = fmt.Errorf("xyzzy") + actual, err := testClientLogs(cs, c) + c.Assert(err, check.ErrorMatches, ".* xyzzy") + c.Check(actual, check.HasLen, 0) +} + +func (cs *clientSuite) TestClientLogsOpts(c *check.C) { + const ( + maxint = int((^uint(0)) >> 1) + minint = -maxint - 1 + ) + for _, names := range [][]string{nil, {}, {"foo"}, {"foo", "bar"}} { + for _, n := range []int{-1, 0, 1, minint, maxint} { + for _, follow := range []bool{true, false} { + iterdesc := check.Commentf("names: %v, n: %v, follow: %v", names, n, follow) + + ch, err := cs.cli.Logs(names, client.LogOptions{N: n, Follow: follow}) + c.Check(err, check.IsNil, iterdesc) + c.Check(cs.req.URL.Path, check.Equals, "/v2/logs", iterdesc) + c.Check(cs.req.Method, check.Equals, "GET", iterdesc) + query := cs.req.URL.Query() + numQ := 0 + + var namesout []string + if ns := query.Get("names"); ns != "" { + namesout = strings.Split(ns, ",") + } + + c.Check(len(namesout), check.Equals, len(names), iterdesc) + if len(names) != 0 { + c.Check(namesout, check.DeepEquals, names, iterdesc) + numQ++ + } + + nout, nerr := strconv.Atoi(query.Get("n")) + c.Check(nerr, check.IsNil, iterdesc) + c.Check(nout, check.Equals, n, iterdesc) + numQ++ + + if follow { + fout, ferr := strconv.ParseBool(query.Get("follow")) + c.Check(fout, check.Equals, true, iterdesc) + c.Check(ferr, check.IsNil, iterdesc) + numQ++ + } + + c.Check(query, check.HasLen, numQ, iterdesc) + + for x := range ch { + c.Logf("expecting empty channel, got %v during %s", x, iterdesc) + c.Fail() + } + } + } + } +} + +func (cs *clientSuite) TestClientServiceStart(c *check.C) { + cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` + + type scenario struct { + names []string + opts client.StartOptions + comment check.CommentInterface + } + + var scenarios []scenario + + for _, names := range [][]string{ + nil, {}, + {"foo"}, + {"foo", "bar", "baz"}, + } { + for _, opts := range []client.StartOptions{ + {Enable: true}, + {Enable: false}, + } { + scenarios = append(scenarios, scenario{ + names: names, + opts: opts, + comment: check.Commentf("{%q; %#v}", names, opts), + }) + } + } + + for _, sc := range scenarios { + id, err := cs.cli.Start(sc.names, sc.opts) + if len(sc.names) == 0 { + c.Check(id, check.Equals, "", sc.comment) + c.Check(err, check.Equals, client.ErrNoNames, sc.comment) + c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done + } else { + c.Assert(err, check.IsNil, sc.comment) + c.Check(id, check.Equals, "24", sc.comment) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment) + c.Check(cs.req.Method, check.Equals, "POST", sc.comment) + c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment) + + inames := make([]interface{}, len(sc.names)) + for i, name := range sc.names { + inames[i] = interface{}(name) + } + + var reqOp map[string]interface{} + c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment) + if sc.opts.Enable { + c.Check(len(reqOp), check.Equals, 3, sc.comment) + c.Check(reqOp["enable"], check.Equals, true, sc.comment) + } else { + c.Check(len(reqOp), check.Equals, 2, sc.comment) + c.Check(reqOp["enable"], check.IsNil, sc.comment) + } + c.Check(reqOp["action"], check.Equals, "start", sc.comment) + c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment) + } + } +} + +func (cs *clientSuite) TestClientServiceStop(c *check.C) { + cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` + + type tT struct { + names []string + opts client.StopOptions + comment check.CommentInterface + } + + var scs []tT + + for _, names := range [][]string{ + nil, {}, + {"foo"}, + {"foo", "bar", "baz"}, + } { + for _, opts := range []client.StopOptions{ + {Disable: true}, + {Disable: false}, + } { + scs = append(scs, tT{ + names: names, + opts: opts, + comment: check.Commentf("{%q; %#v}", names, opts), + }) + } + } + + for _, sc := range scs { + id, err := cs.cli.Stop(sc.names, sc.opts) + if len(sc.names) == 0 { + c.Check(id, check.Equals, "", sc.comment) + c.Check(err, check.Equals, client.ErrNoNames, sc.comment) + c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done + } else { + c.Assert(err, check.IsNil, sc.comment) + c.Check(id, check.Equals, "24", sc.comment) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment) + c.Check(cs.req.Method, check.Equals, "POST", sc.comment) + c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment) + + inames := make([]interface{}, len(sc.names)) + for i, name := range sc.names { + inames[i] = interface{}(name) + } + + var reqOp map[string]interface{} + c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment) + if sc.opts.Disable { + c.Check(len(reqOp), check.Equals, 3, sc.comment) + c.Check(reqOp["disable"], check.Equals, true, sc.comment) + } else { + c.Check(len(reqOp), check.Equals, 2, sc.comment) + c.Check(reqOp["disable"], check.IsNil, sc.comment) + } + c.Check(reqOp["action"], check.Equals, "stop", sc.comment) + c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment) + } + } +} + +func (cs *clientSuite) TestClientServiceRestart(c *check.C) { + cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}` + + type tT struct { + names []string + opts client.RestartOptions + comment check.CommentInterface + } + + var scs []tT + + for _, names := range [][]string{ + nil, {}, + {"foo"}, + {"foo", "bar", "baz"}, + } { + for _, opts := range []client.RestartOptions{ + {Reload: true}, + {Reload: false}, + } { + scs = append(scs, tT{ + names: names, + opts: opts, + comment: check.Commentf("{%q; %#v}", names, opts), + }) + } + } + + for _, sc := range scs { + id, err := cs.cli.Restart(sc.names, sc.opts) + if len(sc.names) == 0 { + c.Check(id, check.Equals, "", sc.comment) + c.Check(err, check.Equals, client.ErrNoNames, sc.comment) + c.Check(cs.req, check.IsNil, sc.comment) // i.e. the request was never done + } else { + c.Assert(err, check.IsNil, sc.comment) + c.Check(id, check.Equals, "24", sc.comment) + c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", sc.comment) + c.Check(cs.req.Method, check.Equals, "POST", sc.comment) + c.Check(cs.req.URL.Query(), check.HasLen, 0, sc.comment) + + inames := make([]interface{}, len(sc.names)) + for i, name := range sc.names { + inames[i] = interface{}(name) + } + + var reqOp map[string]interface{} + c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, sc.comment) + if sc.opts.Reload { + c.Check(len(reqOp), check.Equals, 3, sc.comment) + c.Check(reqOp["reload"], check.Equals, true, sc.comment) + } else { + c.Check(len(reqOp), check.Equals, 2, sc.comment) + c.Check(reqOp["reload"], check.IsNil, sc.comment) + } + c.Check(reqOp["action"], check.Equals, "restart", sc.comment) + c.Check(reqOp["names"], check.DeepEquals, inames, sc.comment) + } + } +} diff --git a/client/asserts.go b/client/asserts.go new file mode 100644 index 00000000..73497ab5 --- /dev/null +++ b/client/asserts.go @@ -0,0 +1,104 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "fmt" + "io" + "net/url" + "strconv" + + "github.com/snapcore/snapd/asserts" // for parsing +) + +// Ack tries to add an assertion to the system assertion +// database. To succeed the assertion must be valid, its signature +// verified with a known public key and the assertion consistent with +// and its prerequisite in the database. +func (client *Client) Ack(b []byte) error { + var rsp interface{} + if _, err := client.doSync("POST", "/v2/assertions", nil, nil, bytes.NewReader(b), &rsp); err != nil { + return err + } + + return nil +} + +// AssertionTypes returns a list of assertion type names. +func (client *Client) AssertionTypes() ([]string, error) { + var types struct { + Types []string `json:"types"` + } + _, err := client.doSync("GET", "/v2/assertions", nil, nil, nil, &types) + if err != nil { + return nil, fmt.Errorf("cannot get assertion type names: %v", err) + } + + return types.Types, nil +} + +// Known queries assertions with type assertTypeName and matching assertion headers. +func (client *Client) Known(assertTypeName string, headers map[string]string) ([]asserts.Assertion, error) { + path := fmt.Sprintf("/v2/assertions/%s", assertTypeName) + q := url.Values{} + + if len(headers) > 0 { + for k, v := range headers { + q.Set(k, v) + } + } + + response, err := client.raw("GET", path, q, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to query assertions: %v", err) + } + defer response.Body.Close() + if response.StatusCode != 200 { + return nil, parseError(response) + } + + sanityCount, err := strconv.Atoi(response.Header.Get("X-Ubuntu-Assertions-Count")) + if err != nil { + return nil, fmt.Errorf("invalid assertions count") + } + + dec := asserts.NewDecoder(response.Body) + + asserts := []asserts.Assertion{} + + // TODO: make sure asserts can decode and deal with unknown types + for { + a, err := dec.Decode() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("failed to decode assertions: %v", err) + } + asserts = append(asserts, a) + } + + if len(asserts) != sanityCount { + return nil, fmt.Errorf("response did not have the expected number of assertions") + } + + return asserts, nil +} diff --git a/client/asserts_test.go b/client/asserts_test.go new file mode 100644 index 00000000..7732d680 --- /dev/null +++ b/client/asserts_test.go @@ -0,0 +1,159 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "errors" + "io/ioutil" + "net/http" + "net/url" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +func (cs *clientSuite) TestClientAssert(c *C) { + cs.rsp = `{ + "type": "sync", + "result": {} + }` + a := []byte("Assertion.") + err := cs.cli.Ack(a) + c.Assert(err, IsNil) + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + c.Check(body, DeepEquals, a) + c.Check(cs.req.Method, Equals, "POST") + c.Check(cs.req.URL.Path, Equals, "/v2/assertions") +} + +func (cs *clientSuite) TestClientAssertsTypes(c *C) { + cs.rsp = `{ + "result": { + "types": ["one", "two"] + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + typs, err := cs.cli.AssertionTypes() + c.Assert(err, IsNil) + c.Check(typs, DeepEquals, []string{"one", "two"}) +} + +func (cs *clientSuite) TestClientAssertsCallsEndpoint(c *C) { + _, _ = cs.cli.Known("snap-revision", nil) + c.Check(cs.req.Method, Equals, "GET") + c.Check(cs.req.URL.Path, Equals, "/v2/assertions/snap-revision") +} + +func (cs *clientSuite) TestClientAssertsCallsEndpointWithFilter(c *C) { + _, _ = cs.cli.Known("snap-revision", map[string]string{ + "snap-id": "snap-id-1", + "snap-sha3-384": "sha3-384...", + }) + u, err := url.ParseRequestURI(cs.req.URL.String()) + c.Assert(err, IsNil) + c.Check(u.Path, Equals, "/v2/assertions/snap-revision") + c.Check(u.Query(), DeepEquals, url.Values{ + "snap-sha3-384": []string{"sha3-384..."}, + "snap-id": []string{"snap-id-1"}, + }) +} + +func (cs *clientSuite) TestClientAssertsHttpError(c *C) { + cs.err = errors.New("fail") + _, err := cs.cli.Known("snap-build", nil) + c.Assert(err, ErrorMatches, "failed to query assertions: cannot communicate with server: fail") +} + +func (cs *clientSuite) TestClientAssertsJSONError(c *C) { + cs.status = 400 + cs.header = http.Header{} + cs.header.Add("Content-type", "application/json") + cs.rsp = `{ + "status-code": 400, + "type": "error", + "result": { + "message": "invalid" + } + }` + _, err := cs.cli.Known("snap-build", nil) + c.Assert(err, ErrorMatches, "invalid") +} + +func (cs *clientSuite) TestClientAsserts(c *C) { + cs.header = http.Header{} + cs.header.Add("X-Ubuntu-Assertions-Count", "2") + cs.rsp = `type: snap-revision +authority-id: store-id1 +snap-sha3-384: P1wNUk5O_5tO5spqOLlqUuAk7gkNYezIMHp5N9hMUg1a6YEjNeaCc4T0BaYz7IWs +snap-id: snap-id-1 +snap-size: 123 +snap-revision: 1 +developer-id: dev-id1 +revision: 1 +timestamp: 2015-11-25T20:00:00Z +body-length: 0 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +openpgp ... + +type: snap-revision +authority-id: store-id1 +snap-sha3-384: 0Yt6-GXQeTZWUAHo1IKDpS9kqO6zMaizY6vGEfGM-aSfpghPKir1Ic7teQ5Zadaj +snap-id: snap-id-2 +snap-size: 456 +snap-revision: 1 +developer-id: dev-id1 +revision: 1 +timestamp: 2015-11-30T20:00:00Z +body-length: 0 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +openpgp ... +` + + a, err := cs.cli.Known("snap-revision", nil) + c.Assert(err, IsNil) + c.Check(a, HasLen, 2) + + c.Check(a[0].Type(), Equals, asserts.SnapRevisionType) +} + +func (cs *clientSuite) TestClientAssertsNoAssertions(c *C) { + cs.header = http.Header{} + cs.header.Add("X-Ubuntu-Assertions-Count", "0") + cs.rsp = "" + cs.status = 200 + a, err := cs.cli.Known("snap-revision", nil) + c.Assert(err, IsNil) + c.Check(a, HasLen, 0) +} + +func (cs *clientSuite) TestClientAssertsMissingAssertions(c *C) { + cs.header = http.Header{} + cs.header.Add("X-Ubuntu-Assertions-Count", "4") + cs.rsp = "" + cs.status = 200 + _, err := cs.cli.Known("snap-build", nil) + c.Assert(err, ErrorMatches, "response did not have the expected number of assertions") +} diff --git a/client/buy.go b/client/buy.go new file mode 100644 index 00000000..d53b850d --- /dev/null +++ b/client/buy.go @@ -0,0 +1,53 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + + "github.com/snapcore/snapd/store" +) + +func (client *Client) Buy(opts *store.BuyOptions) (*store.BuyResult, error) { + if opts == nil { + opts = &store.BuyOptions{} + } + + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(opts); err != nil { + return nil, err + } + + var result store.BuyResult + _, err := client.doSync("POST", "/v2/buy", nil, nil, &body, &result) + + if err != nil { + return nil, err + } + + return &result, nil +} + +func (client *Client) ReadyToBuy() error { + var result bool + _, err := client.doSync("GET", "/v2/buy/ready", nil, nil, nil, &result) + return err +} diff --git a/client/change.go b/client/change.go new file mode 100644 index 00000000..98eed315 --- /dev/null +++ b/client/change.go @@ -0,0 +1,164 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + "time" +) + +// A Change is a modification to the system state. +type Change struct { + ID string `json:"id"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Tasks []*Task `json:"tasks,omitempty"` + Ready bool `json:"ready"` + Err string `json:"err,omitempty"` + + SpawnTime time.Time `json:"spawn-time,omitempty"` + ReadyTime time.Time `json:"ready-time,omitempty"` + + data map[string]*json.RawMessage +} + +var ErrNoData = fmt.Errorf("data entry not found") + +// Get unmarshals into value the kind-specific data with the provided key. +func (c *Change) Get(key string, value interface{}) error { + raw := c.data[key] + if raw == nil { + return ErrNoData + } + return json.Unmarshal([]byte(*raw), value) +} + +// A Task is an operation done to change the system's state. +type Task struct { + ID string `json:"id"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Log []string `json:"log,omitempty"` + Progress TaskProgress `json:"progress"` + + SpawnTime time.Time `json:"spawn-time,omitempty"` + ReadyTime time.Time `json:"ready-time,omitempty"` +} + +type TaskProgress struct { + Label string `json:"label"` + Done int `json:"done"` + Total int `json:"total"` +} + +type changeAndData struct { + Change + Data map[string]*json.RawMessage `json:"data"` +} + +// Change fetches information about a Change given its ID +func (client *Client) Change(id string) (*Change, error) { + var chgd changeAndData + _, err := client.doSync("GET", "/v2/changes/"+id, nil, nil, nil, &chgd) + if err != nil { + return nil, err + } + + chgd.Change.data = chgd.Data + return &chgd.Change, nil +} + +// Abort attempts to abort a change that is in not yet ready. +func (client *Client) Abort(id string) (*Change, error) { + var postData struct { + Action string `json:"action"` + } + postData.Action = "abort" + + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(postData); err != nil { + return nil, err + } + + var chg Change + if _, err := client.doSync("POST", "/v2/changes/"+id, nil, nil, &body, &chg); err != nil { + return nil, err + } + + return &chg, nil +} + +type ChangeSelector uint8 + +func (c ChangeSelector) String() string { + switch c { + case ChangesInProgress: + return "in-progress" + case ChangesReady: + return "ready" + case ChangesAll: + return "all" + } + + panic(fmt.Sprintf("unknown ChangeSelector %d", c)) +} + +const ( + ChangesInProgress ChangeSelector = 1 << iota + ChangesReady + ChangesAll = ChangesReady | ChangesInProgress +) + +type ChangesOptions struct { + SnapName string // if empty, no filtering by name is done + Selector ChangeSelector +} + +func (client *Client) Changes(opts *ChangesOptions) ([]*Change, error) { + query := url.Values{} + if opts != nil { + if opts.Selector != 0 { + query.Set("select", opts.Selector.String()) + } + if opts.SnapName != "" { + query.Set("for", opts.SnapName) + } + } + + var chgds []changeAndData + _, err := client.doSync("GET", "/v2/changes", query, nil, nil, &chgds) + if err != nil { + return nil, err + } + + var chgs []*Change + for i := range chgds { + chgd := &chgds[i] + chgd.Change.data = chgd.Data + chgs = append(chgs, &chgd.Change) + } + + return chgs, err +} diff --git a/client/change_test.go b/client/change_test.go new file mode 100644 index 00000000..79dd9b3b --- /dev/null +++ b/client/change_test.go @@ -0,0 +1,215 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "io/ioutil" + "time" +) + +func (cs *clientSuite) TestClientChange(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "...", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] +}}` + + chg, err := cs.cli.Change("uno") + c.Assert(err, check.IsNil) + c.Check(chg, check.DeepEquals, &client.Change{ + ID: "uno", + Kind: "foo", + Summary: "...", + Status: "Do", + Tasks: []*client.Task{{ + Kind: "bar", + Summary: "...", + Status: "Do", + Progress: client.TaskProgress{Done: 0, Total: 1}, + SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC), + ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC), + }}, + + SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC), + ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC), + }) +} + +func (cs *clientSuite) TestClientChangeData(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "data": {"n": 42} +}}` + + chg, err := cs.cli.Change("uno") + c.Assert(err, check.IsNil) + var n int + err = chg.Get("n", &n) + c.Assert(err, check.IsNil) + c.Assert(n, check.Equals, 42) + + err = chg.Get("missing", &n) + c.Assert(err, check.Equals, client.ErrNoData) +} + +func (cs *clientSuite) TestClientChangeError(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Error", + "ready": true, + "tasks": [{"kind": "bar", "summary": "...", "status": "Error", "progress": {"done": 1, "total": 1}, "log": ["ERROR: something broke"]}], + "err": "error message" +}}` + + chg, err := cs.cli.Change("uno") + c.Assert(err, check.IsNil) + c.Check(chg, check.DeepEquals, &client.Change{ + ID: "uno", + Kind: "foo", + Summary: "...", + Status: "Error", + Tasks: []*client.Task{{ + Kind: "bar", + Summary: "...", + Status: "Error", + Progress: client.TaskProgress{Done: 1, Total: 1}, + Log: []string{"ERROR: something broke"}, + }}, + Err: "error message", + Ready: true, + }) +} + +func (cs *clientSuite) TestClientChangesString(c *check.C) { + for k, v := range map[client.ChangeSelector]string{ + client.ChangesAll: "all", + client.ChangesReady: "ready", + client.ChangesInProgress: "in-progress", + } { + c.Check(k.String(), check.Equals, v) + } +} + +func (cs *clientSuite) TestClientChanges(c *check.C) { + cs.rsp = `{"type": "sync", "result": [{ + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "tasks": [{"kind": "bar", "summary": "...", "status": "Do", "progress": {"done": 0, "total": 1}}] +}]}` + + for _, i := range []*client.ChangesOptions{ + {Selector: client.ChangesAll}, + {Selector: client.ChangesReady}, + {Selector: client.ChangesInProgress}, + {SnapName: "foo"}, + nil, + } { + chg, err := cs.cli.Changes(i) + c.Assert(err, check.IsNil) + c.Check(chg, check.DeepEquals, []*client.Change{{ + ID: "uno", + Kind: "foo", + Summary: "...", + Status: "Do", + Tasks: []*client.Task{{Kind: "bar", Summary: "...", Status: "Do", Progress: client.TaskProgress{Done: 0, Total: 1}}}, + }}) + if i == nil { + c.Check(cs.req.URL.RawQuery, check.Equals, "") + } else { + if i.Selector != 0 { + c.Check(cs.req.URL.RawQuery, check.Equals, "select="+i.Selector.String()) + } else { + c.Check(cs.req.URL.RawQuery, check.Equals, "for="+i.SnapName) + } + } + } + +} + +func (cs *clientSuite) TestClientChangesData(c *check.C) { + cs.rsp = `{"type": "sync", "result": [{ + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "data": {"n": 42} +}]}` + + chgs, err := cs.cli.Changes(&client.ChangesOptions{Selector: client.ChangesAll}) + c.Assert(err, check.IsNil) + + chg := chgs[0] + var n int + err = chg.Get("n", &n) + c.Assert(err, check.IsNil) + c.Assert(n, check.Equals, 42) + + err = chg.Get("missing", &n) + c.Assert(err, check.Equals, client.ErrNoData) +} + +func (cs *clientSuite) TestClientAbort(c *check.C) { + cs.rsp = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Hold", + "ready": true, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z" +}}` + + chg, err := cs.cli.Abort("uno") + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(chg, check.DeepEquals, &client.Change{ + ID: "uno", + Kind: "foo", + Summary: "...", + Status: "Hold", + Ready: true, + + SpawnTime: time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC), + ReadyTime: time.Date(2016, 04, 21, 1, 2, 4, 0, time.UTC), + }) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Equals, "{\"action\":\"abort\"}\n") +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 00000000..7151f387 --- /dev/null +++ b/client/client.go @@ -0,0 +1,576 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path" + "time" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/jsonutil" +) + +func unixDialer(socketPath string) func(string, string) (net.Conn, error) { + if socketPath == "" { + socketPath = dirs.SnapdSocket + } + return func(_, _ string) (net.Conn, error) { + return net.Dial("unix", socketPath) + } +} + +type doer interface { + Do(*http.Request) (*http.Response, error) +} + +// Config allows to customize client behavior. +type Config struct { + // BaseURL contains the base URL where snappy daemon is expected to be. + // It can be empty for a default behavior of talking over a unix socket. + BaseURL string + + // DisableAuth controls whether the client should send an + // Authorization header from reading the auth.json data. + DisableAuth bool + + // Interactive controls whether the client runs in interactive mode. + // At present, this only affects whether interactive polkit + // authorisation is requested. + Interactive bool + + // Socket is the path to the unix socket to use + Socket string +} + +// A Client knows how to talk to the snappy daemon. +type Client struct { + baseURL url.URL + doer doer + + disableAuth bool + interactive bool +} + +// New returns a new instance of Client +func New(config *Config) *Client { + if config == nil { + config = &Config{} + } + + // By default talk over an UNIX socket. + if config.BaseURL == "" { + return &Client{ + baseURL: url.URL{ + Scheme: "http", + Host: "localhost", + }, + doer: &http.Client{ + Transport: &http.Transport{Dial: unixDialer(config.Socket)}, + }, + disableAuth: config.DisableAuth, + interactive: config.Interactive, + } + } + + baseURL, err := url.Parse(config.BaseURL) + if err != nil { + panic(fmt.Sprintf("cannot parse server base URL: %q (%v)", config.BaseURL, err)) + } + return &Client{ + baseURL: *baseURL, + doer: &http.Client{}, + disableAuth: config.DisableAuth, + interactive: config.Interactive, + } +} + +func (client *Client) WhoAmI() (string, error) { + user, err := readAuthData() + if os.IsNotExist(err) { + return "", nil + } + if err != nil { + return "", err + } + + return user.Email, nil +} + +func (client *Client) setAuthorization(req *http.Request) error { + user, err := readAuthData() + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + + var buf bytes.Buffer + fmt.Fprintf(&buf, `Macaroon root="%s"`, user.Macaroon) + for _, discharge := range user.Discharges { + fmt.Fprintf(&buf, `, discharge="%s"`, discharge) + } + req.Header.Set("Authorization", buf.String()) + return nil +} + +type RequestError struct{ error } + +func (e RequestError) Error() string { + return fmt.Sprintf("cannot build request: %v", e.error) +} + +type AuthorizationError struct{ error } + +func (e AuthorizationError) Error() string { + return fmt.Sprintf("cannot add authorization: %v", e.error) +} + +type ConnectionError struct{ error } + +func (e ConnectionError) Error() string { + return fmt.Sprintf("cannot communicate with server: %v", e.error) +} + +// AllowInteractionHeader is the HTTP request header used to indicate +// that the client is willing to allow interaction. +const AllowInteractionHeader = "X-Allow-Interaction" + +// raw performs a request and returns the resulting http.Response and +// error you usually only need to call this directly if you expect the +// response to not be JSON, otherwise you'd call Do(...) instead. +func (client *Client) raw(method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) { + // fake a url to keep http.Client happy + u := client.baseURL + u.Path = path.Join(client.baseURL.Path, urlpath) + u.RawQuery = query.Encode() + req, err := http.NewRequest(method, u.String(), body) + if err != nil { + return nil, RequestError{err} + } + + for key, value := range headers { + req.Header.Set(key, value) + } + + if !client.disableAuth { + // set Authorization header if there are user's credentials + err = client.setAuthorization(req) + if err != nil { + return nil, AuthorizationError{err} + } + } + + if client.interactive { + req.Header.Set(AllowInteractionHeader, "true") + } + + rsp, err := client.doer.Do(req) + if err != nil { + return nil, ConnectionError{err} + } + + return rsp, nil +} + +var ( + doRetry = 250 * time.Millisecond + doTimeout = 5 * time.Second +) + +// MockDoRetry mocks the delays used by the do retry loop. +func MockDoRetry(retry, timeout time.Duration) (restore func()) { + oldRetry := doRetry + oldTimeout := doTimeout + doRetry = retry + doTimeout = timeout + return func() { + doRetry = oldRetry + doTimeout = oldTimeout + } +} + +// do performs a request and decodes the resulting json into the given +// value. It's low-level, for testing/experimenting only; you should +// usually use a higher level interface that builds on this. +func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) error { + retry := time.NewTicker(doRetry) + defer retry.Stop() + timeout := time.After(doTimeout) + var rsp *http.Response + var err error + for { + rsp, err = client.raw(method, path, query, headers, body) + if err == nil || method != "GET" { + break + } + select { + case <-retry.C: + continue + case <-timeout: + } + break + } + if err != nil { + return err + } + defer rsp.Body.Close() + + if v != nil { + dec := json.NewDecoder(rsp.Body) + if err := dec.Decode(v); err != nil { + r := dec.Buffered() + buf, err1 := ioutil.ReadAll(r) + if err1 != nil { + buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1)) + } + return fmt.Errorf("cannot decode %q: %s", buf, err) + } + } + + return nil +} + +// doSync performs a request to the given path using the specified HTTP method. +// It expects a "sync" response from the API and on success decodes the JSON +// response payload into the given value. +func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) { + var rsp response + if err := client.do(method, path, query, headers, body, &rsp); err != nil { + return nil, err + } + if err := rsp.err(); err != nil { + return nil, err + } + if rsp.Type != "sync" { + return nil, fmt.Errorf("expected sync response, got %q", rsp.Type) + } + + if v != nil { + if err := jsonutil.DecodeWithNumber(bytes.NewReader(rsp.Result), v); err != nil { + return nil, fmt.Errorf("cannot unmarshal: %v", err) + } + } + + return &rsp.ResultInfo, nil +} + +func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { + var rsp response + + if err := client.do(method, path, query, headers, body, &rsp); err != nil { + return "", err + } + if err := rsp.err(); err != nil { + return "", err + } + if rsp.Type != "async" { + return "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type) + } + if rsp.StatusCode != 202 { + return "", fmt.Errorf("operation not accepted") + } + if rsp.Change == "" { + return "", fmt.Errorf("async response without change reference") + } + + return rsp.Change, nil +} + +type ServerVersion struct { + Version string + Series string + OSID string + OSVersionID string + OnClassic bool + + KernelVersion string +} + +func (client *Client) ServerVersion() (*ServerVersion, error) { + sysInfo, err := client.SysInfo() + if err != nil { + return nil, err + } + + return &ServerVersion{ + Version: sysInfo.Version, + Series: sysInfo.Series, + OSID: sysInfo.OSRelease.ID, + OSVersionID: sysInfo.OSRelease.VersionID, + OnClassic: sysInfo.OnClassic, + + KernelVersion: sysInfo.KernelVersion, + }, nil +} + +// A response produced by the REST API will usually fit in this +// (exceptions are the icons/ endpoints obvs) +type response struct { + Result json.RawMessage `json:"result"` + Status string `json:"status"` + StatusCode int `json:"status-code"` + Type string `json:"type"` + Change string `json:"change"` + + ResultInfo +} + +// Error is the real value of response.Result when an error occurs. +type Error struct { + Kind string `json:"kind"` + Value interface{} `json:"value"` + Message string `json:"message"` + + StatusCode int +} + +func (e *Error) Error() string { + return e.Message +} + +const ( + ErrorKindTwoFactorRequired = "two-factor-required" + ErrorKindTwoFactorFailed = "two-factor-failed" + ErrorKindLoginRequired = "login-required" + ErrorKindTermsNotAccepted = "terms-not-accepted" + ErrorKindNoPaymentMethods = "no-payment-methods" + ErrorKindPaymentDeclined = "payment-declined" + ErrorKindPasswordPolicy = "password-policy" + + ErrorKindSnapAlreadyInstalled = "snap-already-installed" + ErrorKindSnapNotInstalled = "snap-not-installed" + ErrorKindSnapNotFound = "snap-not-found" + ErrorKindSnapLocal = "snap-local" + ErrorKindSnapNeedsDevMode = "snap-needs-devmode" + ErrorKindSnapNeedsClassic = "snap-needs-classic" + ErrorKindSnapNeedsClassicSystem = "snap-needs-classic-system" + ErrorKindNoUpdateAvailable = "snap-no-update-available" + + ErrorKindNotSnap = "snap-not-a-snap" +) + +// IsTwoFactorError returns whether the given error is due to problems +// in two-factor authentication. +func IsTwoFactorError(err error) bool { + e, ok := err.(*Error) + if !ok || e == nil { + return false + } + + return e.Kind == ErrorKindTwoFactorFailed || e.Kind == ErrorKindTwoFactorRequired +} + +// OSRelease contains information about the system extracted from /etc/os-release. +type OSRelease struct { + ID string `json:"id"` + VersionID string `json:"version-id,omitempty"` +} + +type RefreshInfo struct { + Schedule string `json:"schedule"` + Last string `json:"last,omitempty"` + Next string `json:"next,omitempty"` +} + +// SysInfo holds system information +type SysInfo struct { + Series string `json:"series,omitempty"` + Version string `json:"version,omitempty"` + OSRelease OSRelease `json:"os-release"` + OnClassic bool `json:"on-classic"` + Managed bool `json:"managed"` + + KernelVersion string `json:"kernel-version,omitempty"` + + Refresh RefreshInfo `json:"refresh,omitempty"` + Confinement string `json:"confinement"` +} + +func (rsp *response) err() error { + if rsp.Type != "error" { + return nil + } + var resultErr Error + err := json.Unmarshal(rsp.Result, &resultErr) + if err != nil || resultErr.Message == "" { + return fmt.Errorf("server error: %q", rsp.Status) + } + resultErr.StatusCode = rsp.StatusCode + + return &resultErr +} + +func parseError(r *http.Response) error { + var rsp response + if r.Header.Get("Content-Type") != "application/json" { + return fmt.Errorf("server error: %q", r.Status) + } + + dec := json.NewDecoder(r.Body) + if err := dec.Decode(&rsp); err != nil { + return fmt.Errorf("cannot unmarshal error: %v", err) + } + + err := rsp.err() + if err == nil { + return fmt.Errorf("server error: %q", r.Status) + } + return err +} + +// SysInfo gets system information from the REST API. +func (client *Client) SysInfo() (*SysInfo, error) { + var sysInfo SysInfo + + if _, err := client.doSync("GET", "/v2/system-info", nil, nil, nil, &sysInfo); err != nil { + return nil, fmt.Errorf("cannot obtain system details: %v", err) + } + + return &sysInfo, nil +} + +// CreateUserResult holds the result of a user creation. +type CreateUserResult struct { + Username string `json:"username"` + SSHKeys []string `json:"ssh-keys"` +} + +// CreateUserOptions holds options for creating a local system user. +// +// If Known is false, the provided email is used to query the store for +// username and SSH key details. +// +// If Known is true, the user will be created by looking through existing +// system-user assertions and looking for a matching email. If Email is +// empty then all such assertions are considered and multiple users may +// be created. +type CreateUserOptions struct { + Email string `json:"email,omitempty"` + Sudoer bool `json:"sudoer,omitempty"` + Known bool `json:"known,omitempty"` + ForceManaged bool `json:"force-managed,omitempty"` +} + +// CreateUser creates a local system user. See CreateUserOptions for details. +func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) { + if options.Email == "" { + return nil, fmt.Errorf("cannot create a user without providing an email") + } + + var result CreateUserResult + data, err := json.Marshal(options) + if err != nil { + return nil, err + } + + if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { + return nil, fmt.Errorf("while creating user: %v", err) + } + return &result, nil +} + +// CreateUsers creates multiple local system users. See CreateUserOptions for details. +// +// Results may be provided even if there are errors. +func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) { + for _, opts := range options { + if opts.Email == "" && !opts.Known { + return nil, fmt.Errorf("cannot create user from store details without an email to query for") + } + } + + var results []*CreateUserResult + var errs []error + + for _, opts := range options { + data, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + if opts.Email == "" { + var result []*CreateUserResult + if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { + errs = append(errs, err) + } else { + results = append(results, result...) + } + } else { + var result *CreateUserResult + if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { + errs = append(errs, err) + } else { + results = append(results, result) + } + } + } + + if len(errs) == 1 { + return results, errs[0] + } + if len(errs) > 1 { + var buf bytes.Buffer + for _, err := range errs { + fmt.Fprintf(&buf, "\n- %s", err) + } + return results, fmt.Errorf("while creating users:%s", buf.Bytes()) + } + return results, nil +} + +// Users returns the local users. +func (client *Client) Users() ([]*User, error) { + var result []*User + + if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil { + return nil, fmt.Errorf("while getting users: %v", err) + } + return result, nil +} + +type debugAction struct { + Action string `json:"action"` + Params interface{} `json:"params,omitempty"` +} + +// Debug is only useful when writing test code, it will trigger +// an internal action with the given parameters. +func (client *Client) Debug(action string, params interface{}, result interface{}) error { + body, err := json.Marshal(debugAction{ + Action: action, + Params: params, + }) + if err != nil { + return err + } + + _, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result) + return err +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 00000000..6733a54b --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,513 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type clientSuite struct { + cli *client.Client + req *http.Request + reqs []*http.Request + rsp string + rsps []string + err error + doCalls int + header http.Header + status int +} + +var _ = Suite(&clientSuite{}) + +func (cs *clientSuite) SetUpTest(c *C) { + os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "auth.json")) + cs.cli = client.New(nil) + cs.cli.SetDoer(cs) + cs.err = nil + cs.req = nil + cs.reqs = nil + cs.rsp = "" + cs.rsps = nil + cs.req = nil + cs.header = nil + cs.status = 200 + cs.doCalls = 0 + + dirs.SetRootDir(c.MkDir()) +} + +func (cs *clientSuite) TearDownTest(c *C) { + os.Unsetenv(client.TestAuthFileEnvKey) +} + +func (cs *clientSuite) Do(req *http.Request) (*http.Response, error) { + cs.req = req + cs.reqs = append(cs.reqs, req) + body := cs.rsp + if cs.doCalls < len(cs.rsps) { + body = cs.rsps[cs.doCalls] + } + rsp := &http.Response{ + Body: ioutil.NopCloser(strings.NewReader(body)), + Header: cs.header, + StatusCode: cs.status, + } + cs.doCalls++ + return rsp, cs.err +} + +func (cs *clientSuite) TestNewPanics(c *C) { + c.Assert(func() { + client.New(&client.Config{BaseURL: ":"}) + }, PanicMatches, `cannot parse server base URL: ":" \(parse :: missing protocol scheme\)`) +} + +func (cs *clientSuite) TestClientDoReportsErrors(c *C) { + restore := client.MockDoRetry(10*time.Millisecond, 100*time.Millisecond) + defer restore() + cs.err = errors.New("ouchie") + err := cs.cli.Do("GET", "/", nil, nil, nil) + c.Check(err, ErrorMatches, "cannot communicate with server: ouchie") + if cs.doCalls < 2 { + c.Fatalf("do did not retry") + } +} + +func (cs *clientSuite) TestClientWorks(c *C) { + var v []int + cs.rsp = `[1,2]` + reqBody := ioutil.NopCloser(strings.NewReader("")) + err := cs.cli.Do("GET", "/this", nil, reqBody, &v) + c.Check(err, IsNil) + c.Check(v, DeepEquals, []int{1, 2}) + c.Assert(cs.req, NotNil) + c.Assert(cs.req.URL, NotNil) + c.Check(cs.req.Method, Equals, "GET") + c.Check(cs.req.Body, Equals, reqBody) + c.Check(cs.req.URL.Path, Equals, "/this") +} + +func (cs *clientSuite) TestClientDefaultsToNoAuthorization(c *C) { + os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json")) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + var v string + _ = cs.cli.Do("GET", "/this", nil, nil, &v) + c.Assert(cs.req, NotNil) + authorization := cs.req.Header.Get("Authorization") + c.Check(authorization, Equals, "") +} + +func (cs *clientSuite) TestClientSetsAuthorization(c *C) { + os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json")) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + mockUserData := client.User{ + Macaroon: "macaroon", + Discharges: []string{"discharge"}, + } + err := client.TestWriteAuth(mockUserData) + c.Assert(err, IsNil) + + var v string + _ = cs.cli.Do("GET", "/this", nil, nil, &v) + authorization := cs.req.Header.Get("Authorization") + c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`) +} + +func (cs *clientSuite) TestClientHonorsDisableAuth(c *C) { + os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json")) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + mockUserData := client.User{ + Macaroon: "macaroon", + Discharges: []string{"discharge"}, + } + err := client.TestWriteAuth(mockUserData) + c.Assert(err, IsNil) + + var v string + cli := client.New(&client.Config{DisableAuth: true}) + cli.SetDoer(cs) + _ = cli.Do("GET", "/this", nil, nil, &v) + authorization := cs.req.Header.Get("Authorization") + c.Check(authorization, Equals, "") +} + +func (cs *clientSuite) TestClientHonorsInteractive(c *C) { + var v string + cli := client.New(&client.Config{Interactive: false}) + cli.SetDoer(cs) + _ = cli.Do("GET", "/this", nil, nil, &v) + interactive := cs.req.Header.Get(client.AllowInteractionHeader) + c.Check(interactive, Equals, "") + + cli = client.New(&client.Config{Interactive: true}) + cli.SetDoer(cs) + _ = cli.Do("GET", "/this", nil, nil, &v) + interactive = cs.req.Header.Get(client.AllowInteractionHeader) + c.Check(interactive, Equals, "true") +} + +func (cs *clientSuite) TestClientWhoAmINobody(c *C) { + email, err := cs.cli.WhoAmI() + c.Assert(err, IsNil) + c.Check(email, Equals, "") +} + +func (cs *clientSuite) TestClientWhoAmIRubbish(c *C) { + c.Assert(ioutil.WriteFile(client.TestStoreAuthFilename(os.Getenv("HOME")), []byte("rubbish"), 0644), IsNil) + + email, err := cs.cli.WhoAmI() + c.Check(err, NotNil) + c.Check(email, Equals, "") +} + +func (cs *clientSuite) TestClientWhoAmISomebody(c *C) { + mockUserData := client.User{ + Email: "foo@example.com", + } + c.Assert(client.TestWriteAuth(mockUserData), IsNil) + + email, err := cs.cli.WhoAmI() + c.Check(err, IsNil) + c.Check(email, Equals, "foo@example.com") +} + +func (cs *clientSuite) TestClientSysInfo(c *C) { + cs.rsp = `{"type": "sync", "result": + {"series": "16", + "version": "2", + "os-release": {"id": "ubuntu", "version-id": "16.04"}, + "on-classic": true, + "confinement": "strict"}}` + sysInfo, err := cs.cli.SysInfo() + c.Check(err, IsNil) + c.Check(sysInfo, DeepEquals, &client.SysInfo{ + Version: "2", + Series: "16", + OSRelease: client.OSRelease{ + ID: "ubuntu", + VersionID: "16.04", + }, + OnClassic: true, + Confinement: "strict", + }) +} + +func (cs *clientSuite) TestServerVersion(c *C) { + cs.rsp = `{"type": "sync", "result": + {"series": "16", + "version": "2", + "os-release": {"id": "zyggy", "version-id": "123"}}}` + version, err := cs.cli.ServerVersion() + c.Check(err, IsNil) + c.Check(version, DeepEquals, &client.ServerVersion{ + Version: "2", + Series: "16", + OSID: "zyggy", + OSVersionID: "123", + }) +} + +func (cs *clientSuite) TestSnapdClientIntegration(c *C) { + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdSocket), 0755), IsNil) + l, err := net.Listen("unix", dirs.SnapdSocket) + if err != nil { + c.Fatalf("unable to listen on %q: %v", dirs.SnapdSocket, err) + } + + f := func(w http.ResponseWriter, r *http.Request) { + c.Check(r.URL.Path, Equals, "/v2/system-info") + c.Check(r.URL.RawQuery, Equals, "") + + fmt.Fprintln(w, `{"type":"sync", "result":{"series":"42"}}`) + } + + srv := &httptest.Server{ + Listener: l, + Config: &http.Server{Handler: http.HandlerFunc(f)}, + } + srv.Start() + defer srv.Close() + + cli := client.New(nil) + si, err := cli.SysInfo() + c.Check(err, IsNil) + c.Check(si.Series, Equals, "42") +} + +func (cs *clientSuite) TestSnapClientIntegration(c *C) { + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapSocket), 0755), IsNil) + l, err := net.Listen("unix", dirs.SnapSocket) + if err != nil { + c.Fatalf("unable to listen on %q: %v", dirs.SnapSocket, err) + } + + f := func(w http.ResponseWriter, r *http.Request) { + c.Check(r.URL.Path, Equals, "/v2/snapctl") + c.Check(r.URL.RawQuery, Equals, "") + + fmt.Fprintln(w, `{"type":"sync", "result":{"stdout":"test stdout","stderr":"test stderr"}}`) + } + + srv := &httptest.Server{ + Listener: l, + Config: &http.Server{Handler: http.HandlerFunc(f)}, + } + srv.Start() + defer srv.Close() + + cli := client.New(&client.Config{ + Socket: dirs.SnapSocket, + }) + options := &client.SnapCtlOptions{ + ContextID: "foo", + Args: []string{"bar", "--baz"}, + } + + stdout, stderr, err := cli.RunSnapctl(options) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "test stdout") + c.Check(string(stderr), Equals, "test stderr") +} + +func (cs *clientSuite) TestClientReportsOpError(c *C) { + cs.rsp = `{"type": "error", "status": "potatoes"}` + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*server error: "potatoes"`) +} + +func (cs *clientSuite) TestClientReportsOpErrorStr(c *C) { + cs.rsp = `{ + "result": {}, + "status": "Bad Request", + "status-code": 400, + "type": "error" + }` + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*server error: "Bad Request"`) +} + +func (cs *clientSuite) TestClientReportsBadType(c *C) { + cs.rsp = `{"type": "what"}` + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*expected sync response, got "what"`) +} + +func (cs *clientSuite) TestClientReportsOuterJSONError(c *C) { + cs.rsp = "this isn't really json is it" + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*invalid character .*`) +} + +func (cs *clientSuite) TestClientReportsInnerJSONError(c *C) { + cs.rsp = `{"type": "sync", "result": "this isn't really json is it"}` + _, err := cs.cli.SysInfo() + c.Check(err, ErrorMatches, `.*cannot unmarshal.*`) +} + +func (cs *clientSuite) TestParseError(c *C) { + resp := &http.Response{ + Status: "404 Not Found", + } + err := client.ParseErrorInTest(resp) + c.Check(err, ErrorMatches, `server error: "404 Not Found"`) + + h := http.Header{} + h.Add("Content-Type", "application/json") + resp = &http.Response{ + Status: "400 Bad Request", + Header: h, + Body: ioutil.NopCloser(strings.NewReader(`{ + "status-code": 400, + "type": "error", + "result": { + "message": "invalid" + } + }`)), + } + err = client.ParseErrorInTest(resp) + c.Check(err, ErrorMatches, "invalid") + + resp = &http.Response{ + Status: "400 Bad Request", + Header: h, + Body: ioutil.NopCloser(strings.NewReader("{}")), + } + err = client.ParseErrorInTest(resp) + c.Check(err, ErrorMatches, `server error: "400 Bad Request"`) +} + +func (cs *clientSuite) TestIsTwoFactor(c *C) { + c.Check(client.IsTwoFactorError(&client.Error{Kind: client.ErrorKindTwoFactorRequired}), Equals, true) + c.Check(client.IsTwoFactorError(&client.Error{Kind: client.ErrorKindTwoFactorFailed}), Equals, true) + c.Check(client.IsTwoFactorError(&client.Error{Kind: "some other kind"}), Equals, false) + c.Check(client.IsTwoFactorError(errors.New("test")), Equals, false) + c.Check(client.IsTwoFactorError(nil), Equals, false) + c.Check(client.IsTwoFactorError((*client.Error)(nil)), Equals, false) +} + +func (cs *clientSuite) TestClientCreateUser(c *C) { + _, err := cs.cli.CreateUser(&client.CreateUserOptions{}) + c.Assert(err, ErrorMatches, "cannot create a user without providing an email") + + cs.rsp = `{ + "type": "sync", + "result": { + "username": "karl", + "ssh-keys": ["one", "two"] + } + }` + rsp, err := cs.cli.CreateUser(&client.CreateUserOptions{Email: "one@email.com", Sudoer: true, Known: true}) + c.Assert(cs.req.Method, Equals, "POST") + c.Assert(cs.req.URL.Path, Equals, "/v2/create-user") + c.Assert(err, IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + c.Assert(string(body), Equals, `{"email":"one@email.com","sudoer":true,"known":true}`) + + c.Assert(rsp, DeepEquals, &client.CreateUserResult{ + Username: "karl", + SSHKeys: []string{"one", "two"}, + }) +} + +var createUsersTests = []struct { + options []*client.CreateUserOptions + bodies []string + responses []string + results []*client.CreateUserResult + error string +}{{ + options: []*client.CreateUserOptions{{}}, + error: "cannot create user from store details without an email to query for", +}, { + options: []*client.CreateUserOptions{{ + Email: "one@example.com", + Sudoer: true, + }, { + Known: true, + }}, + bodies: []string{ + `{"email":"one@example.com","sudoer":true}`, + `{"known":true}`, + }, + responses: []string{ + `{"type": "sync", "result": {"username": "one", "ssh-keys":["a", "b"]}}`, + `{"type": "sync", "result": [{"username": "two"}, {"username": "three"}]}`, + }, + results: []*client.CreateUserResult{{ + Username: "one", + SSHKeys: []string{"a", "b"}, + }, { + Username: "two", + }, { + Username: "three", + }}, +}} + +func (cs *clientSuite) TestClientCreateUsers(c *C) { + for _, test := range createUsersTests { + cs.rsps = test.responses + + results, err := cs.cli.CreateUsers(test.options) + if test.error != "" { + c.Assert(err, ErrorMatches, test.error) + } + c.Assert(results, DeepEquals, test.results) + + var bodies []string + for _, req := range cs.reqs { + c.Assert(req.Method, Equals, "POST") + c.Assert(req.URL.Path, Equals, "/v2/create-user") + data, err := ioutil.ReadAll(req.Body) + c.Assert(err, IsNil) + bodies = append(bodies, string(data)) + } + + c.Assert(bodies, DeepEquals, test.bodies) + } +} + +func (cs *clientSuite) TestClientJSONError(c *C) { + cs.rsp = `some non-json error message` + _, err := cs.cli.SysInfo() + c.Assert(err, ErrorMatches, `cannot obtain system details: cannot decode "some non-json error message": invalid char.*`) +} + +func (cs *clientSuite) TestUsers(c *C) { + cs.rsp = `{"type": "sync", "result": + [{"username": "foo","email":"foo@example.com"}, + {"username": "bar","email":"bar@example.com"}]}` + users, err := cs.cli.Users() + c.Check(err, IsNil) + c.Check(users, DeepEquals, []*client.User{ + {Username: "foo", Email: "foo@example.com"}, + {Username: "bar", Email: "bar@example.com"}, + }) +} + +func (cs *clientSuite) TestDebugEnsureStateSoon(c *C) { + cs.rsp = `{"type": "sync", "result":true}` + err := cs.cli.Debug("ensure-state-soon", nil, nil) + c.Check(err, IsNil) + c.Check(cs.reqs, HasLen, 1) + c.Check(cs.reqs[0].Method, Equals, "POST") + c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug") + data, err := ioutil.ReadAll(cs.reqs[0].Body) + c.Assert(err, IsNil) + c.Check(data, DeepEquals, []byte(`{"action":"ensure-state-soon"}`)) +} + +func (cs *clientSuite) TestDebugGeneric(c *C) { + cs.rsp = `{"type": "sync", "result":["res1","res2"]}` + + var result []string + err := cs.cli.Debug("do-something", []string{"param1", "param2"}, &result) + c.Check(err, IsNil) + c.Check(result, DeepEquals, []string{"res1", "res2"}) + c.Check(cs.reqs, HasLen, 1) + c.Check(cs.reqs[0].Method, Equals, "POST") + c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug") + data, err := ioutil.ReadAll(cs.reqs[0].Body) + c.Assert(err, IsNil) + c.Check(string(data), DeepEquals, `{"action":"do-something","params":["param1","param2"]}`) +} diff --git a/client/conf.go b/client/conf.go new file mode 100644 index 00000000..1fd60944 --- /dev/null +++ b/client/conf.go @@ -0,0 +1,50 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "net/url" + "strings" +) + +// SetConf requests a snap to apply the provided patch to the configuration. +func (client *Client) SetConf(snapName string, patch map[string]interface{}) (changeID string, err error) { + b, err := json.Marshal(patch) + if err != nil { + return "", err + } + return client.doAsync("PUT", "/v2/snaps/"+snapName+"/conf", nil, nil, bytes.NewReader(b)) +} + +// Conf asks for a snap's current configuration. +func (client *Client) Conf(snapName string, keys []string) (configuration map[string]interface{}, err error) { + // Prepare query + query := url.Values{} + query.Set("keys", strings.Join(keys, ",")) + + _, err = client.doSync("GET", "/v2/snaps/"+snapName+"/conf", query, nil, nil, &configuration) + if err != nil { + return nil, err + } + + return configuration, nil +} diff --git a/client/conf_test.go b/client/conf_test.go new file mode 100644 index 00000000..6a499584 --- /dev/null +++ b/client/conf_test.go @@ -0,0 +1,104 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "encoding/json" + + "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientSetConfCallsEndpoint(c *check.C) { + cs.cli.SetConf("snap-name", map[string]interface{}{"key": "value"}) + c.Check(cs.req.Method, check.Equals, "PUT") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf") +} + +func (cs *clientSuite) TestClientGetConfCallsEndpoint(c *check.C) { + cs.cli.Conf("snap-name", []string{"test-key"}) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf") + c.Check(cs.req.URL.Query().Get("keys"), check.Equals, "test-key") +} + +func (cs *clientSuite) TestClientGetConfCallsEndpointMultipleKeys(c *check.C) { + cs.cli.Conf("snap-name", []string{"test-key1", "test-key2"}) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps/snap-name/conf") + c.Check(cs.req.URL.Query().Get("keys"), check.Equals, "test-key1,test-key2") +} + +func (cs *clientSuite) TestClientSetConf(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "foo" + }` + id, err := cs.cli.SetConf("snap-name", map[string]interface{}{"key": "value"}) + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "foo") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "key": "value", + }) +} + +func (cs *clientSuite) TestClientGetConf(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {"test-key": "test-value"} + }` + value, err := cs.cli.Conf("snap-name", []string{"test-key"}) + c.Assert(err, check.IsNil) + c.Check(value, check.DeepEquals, map[string]interface{}{"test-key": "test-value"}) +} + +func (cs *clientSuite) TestClientGetConfBigInt(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {"test-key": 1234567890} + }` + value, err := cs.cli.Conf("snap-name", []string{"test-key"}) + c.Assert(err, check.IsNil) + c.Check(value, check.DeepEquals, map[string]interface{}{"test-key": json.Number("1234567890")}) +} + +func (cs *clientSuite) TestClientGetConfMultipleKeys(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { + "test-key1": "test-value1", + "test-key2": "test-value2" + } + }` + value, err := cs.cli.Conf("snap-name", []string{"test-key1", "test-key2"}) + c.Assert(err, check.IsNil) + c.Check(value, check.DeepEquals, map[string]interface{}{ + "test-key1": "test-value1", + "test-key2": "test-value2", + }) +} diff --git a/client/export_test.go b/client/export_test.go new file mode 100644 index 00000000..ff750c86 --- /dev/null +++ b/client/export_test.go @@ -0,0 +1,45 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "io" + "net/url" +) + +// SetDoer sets the client's doer to the given one +func (client *Client) SetDoer(d doer) { + client.doer = d +} + +// Do does do. +func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}) error { + return client.do(method, path, query, nil, body, v) +} + +// expose parseError for testing +var ParseErrorInTest = parseError + +// expose read and write auth helpers for testing +var TestWriteAuth = writeAuthData +var TestReadAuth = readAuthData +var TestStoreAuthFilename = storeAuthDataFilename + +var TestAuthFileEnvKey = authFileEnvKey diff --git a/client/icons.go b/client/icons.go new file mode 100644 index 00000000..abde3604 --- /dev/null +++ b/client/icons.go @@ -0,0 +1,66 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "fmt" + "io/ioutil" + "regexp" +) + +// Icon represents the icon of an installed snap +type Icon struct { + Filename string + Content []byte +} + +// Icon returns the Icon belonging to an installed snap +func (c *Client) Icon(pkgID string) (*Icon, error) { + const errPrefix = "cannot retrieve icon" + + response, err := c.raw("GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil) + if err != nil { + return nil, fmt.Errorf("%s: failed to communicate with server: %s", errPrefix, err) + } + defer response.Body.Close() + + if response.StatusCode != 200 { + return nil, fmt.Errorf("%s: Not Found", errPrefix) + } + + re := regexp.MustCompile(`attachment; filename=(.+)`) + matches := re.FindStringSubmatch(response.Header.Get("Content-Disposition")) + + if matches == nil || matches[1] == "" { + return nil, fmt.Errorf("%s: cannot determine filename", errPrefix) + } + + content, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("%s: %s", errPrefix, err) + } + + icon := &Icon{ + Filename: matches[1], + Content: content, + } + + return icon, nil +} diff --git a/client/icons_test.go b/client/icons_test.go new file mode 100644 index 00000000..2dbffb15 --- /dev/null +++ b/client/icons_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "errors" + "fmt" + "net/http" + + . "gopkg.in/check.v1" +) + +const ( + pkgID = "chatroom.ogra" +) + +func (cs *clientSuite) TestClientIconCallsEndpoint(c *C) { + _, _ = cs.cli.Icon(pkgID) + c.Assert(cs.req.Method, Equals, "GET") + c.Assert(cs.req.URL.Path, Equals, fmt.Sprintf("/v2/icons/%s/icon", pkgID)) +} + +func (cs *clientSuite) TestClientIconHttpError(c *C) { + cs.err = errors.New("fail") + _, err := cs.cli.Icon(pkgID) + c.Assert(err, ErrorMatches, ".*server: fail") +} + +func (cs *clientSuite) TestClientIconResponseNotFound(c *C) { + cs.status = 404 + _, err := cs.cli.Icon(pkgID) + c.Assert(err, ErrorMatches, `.*Not Found`) +} + +func (cs *clientSuite) TestClientIconInvalidContentDisposition(c *C) { + cs.header = http.Header{"Content-Disposition": {"invalid"}} + _, err := cs.cli.Icon(pkgID) + c.Assert(err, ErrorMatches, `.*cannot determine filename`) +} + +func (cs *clientSuite) TestClientIcon(c *C) { + cs.rsp = "pixels" + cs.header = http.Header{"Content-Disposition": {"attachment; filename=myicon.png"}} + icon, err := cs.cli.Icon(pkgID) + c.Assert(err, IsNil) + c.Assert(icon.Filename, Equals, "myicon.png") + c.Assert(icon.Content, DeepEquals, []byte("pixels")) +} diff --git a/client/interfaces.go b/client/interfaces.go new file mode 100644 index 00000000..4a79d78f --- /dev/null +++ b/client/interfaces.go @@ -0,0 +1,155 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "net/url" + "strings" +) + +// Plug represents the potential of a given snap to connect to a slot. +type Plug struct { + Snap string `json:"snap"` + Name string `json:"plug"` + Interface string `json:"interface,omitempty"` + Attrs map[string]interface{} `json:"attrs,omitempty"` + Apps []string `json:"apps,omitempty"` + Label string `json:"label,omitempty"` + Connections []SlotRef `json:"connections,omitempty"` +} + +// PlugRef is a reference to a plug. +type PlugRef struct { + Snap string `json:"snap"` + Name string `json:"plug"` +} + +// Slot represents a capacity offered by a snap. +type Slot struct { + Snap string `json:"snap"` + Name string `json:"slot"` + Interface string `json:"interface,omitempty"` + Attrs map[string]interface{} `json:"attrs,omitempty"` + Apps []string `json:"apps,omitempty"` + Label string `json:"label,omitempty"` + Connections []PlugRef `json:"connections,omitempty"` +} + +// SlotRef is a reference to a slot. +type SlotRef struct { + Snap string `json:"snap"` + Name string `json:"slot"` +} + +// Connections contains information about all plugs, slots and their connections +type Connections struct { + Plugs []Plug `json:"plugs"` + Slots []Slot `json:"slots"` +} + +// Interface holds information about a given interface and its instances. +type Interface struct { + Name string `json:"name,omitempty"` + Summary string `json:"summary,omitempty"` + DocURL string `json:"doc-url,omitempty"` + Plugs []Plug `json:"plugs,omitempty"` + Slots []Slot `json:"slots,omitempty"` +} + +// InterfaceAction represents an action performed on the interface system. +type InterfaceAction struct { + Action string `json:"action"` + Plugs []Plug `json:"plugs,omitempty"` + Slots []Slot `json:"slots,omitempty"` +} + +// Connections returns all plugs, slots and their connections. +func (client *Client) Connections() (Connections, error) { + var conns Connections + _, err := client.doSync("GET", "/v2/interfaces", nil, nil, nil, &conns) + return conns, err +} + +// InterfaceOptions represents opt-in elements include in responses. +type InterfaceOptions struct { + Names []string + Doc bool + Plugs bool + Slots bool + Connected bool +} + +func (client *Client) Interfaces(opts *InterfaceOptions) ([]*Interface, error) { + query := url.Values{} + if opts != nil && len(opts.Names) > 0 { + query.Set("names", strings.Join(opts.Names, ",")) // Return just those specific interfaces. + } + if opts != nil { + if opts.Doc { + query.Set("doc", "true") // Return documentation of each selected interface. + } + if opts.Plugs { + query.Set("plugs", "true") // Return plugs of each selected interface. + } + if opts.Slots { + query.Set("slots", "true") // Return slots of each selected interface. + } + } + // NOTE: Presence of "select" triggers the use of the new response format. + if opts != nil && opts.Connected { + query.Set("select", "connected") // Return just the connected interfaces. + } else { + query.Set("select", "all") // Return all interfaces. + } + var interfaces []*Interface + _, err := client.doSync("GET", "/v2/interfaces", query, nil, nil, &interfaces) + + return interfaces, err +} + +// performInterfaceAction performs a single action on the interface system. +func (client *Client) performInterfaceAction(sa *InterfaceAction) (changeID string, err error) { + b, err := json.Marshal(sa) + if err != nil { + return "", err + } + return client.doAsync("POST", "/v2/interfaces", nil, nil, bytes.NewReader(b)) +} + +// Connect establishes a connection between a plug and a slot. +// The plug and the slot must have the same interface. +func (client *Client) Connect(plugSnapName, plugName, slotSnapName, slotName string) (changeID string, err error) { + return client.performInterfaceAction(&InterfaceAction{ + Action: "connect", + Plugs: []Plug{{Snap: plugSnapName, Name: plugName}}, + Slots: []Slot{{Snap: slotSnapName, Name: slotName}}, + }) +} + +// Disconnect breaks the connection between a plug and a slot. +func (client *Client) Disconnect(plugSnapName, plugName, slotSnapName, slotName string) (changeID string, err error) { + return client.performInterfaceAction(&InterfaceAction{ + Action: "disconnect", + Plugs: []Plug{{Snap: plugSnapName, Name: plugName}}, + Slots: []Slot{{Snap: slotSnapName, Name: slotName}}, + }) +} diff --git a/client/interfaces_test.go b/client/interfaces_test.go new file mode 100644 index 00000000..38b7d275 --- /dev/null +++ b/client/interfaces_test.go @@ -0,0 +1,299 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "encoding/json" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) TestClientInterfacesOptionEncoding(c *check.C) { + // Choose some options + _, _ = cs.cli.Interfaces(&client.InterfaceOptions{ + Names: []string{"a", "b"}, + Doc: true, + Plugs: true, + Slots: true, + Connected: true, + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + c.Check(cs.req.URL.RawQuery, check.Equals, + "doc=true&names=a%2Cb&plugs=true&select=connected&slots=true") +} + +func (cs *clientSuite) TestClientInterfacesAll(c *check.C) { + // Ask for a summary of all interfaces. + cs.rsp = `{ + "type": "sync", + "result": [ + {"name": "iface-a", "summary": "the A iface"}, + {"name": "iface-b", "summary": "the B iface"}, + {"name": "iface-c", "summary": "the C iface"} + ] + }` + ifaces, err := cs.cli.Interfaces(nil) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + // This uses the select=all query option to indicate that new response + // format should be used. The same API endpoint is used by the Interfaces + // and by the Connections functions an the absence or presence of the + // select query option decides what kind of result should be returned + // (legacy or modern). + c.Check(cs.req.URL.RawQuery, check.Equals, "select=all") + c.Assert(err, check.IsNil) + c.Check(ifaces, check.DeepEquals, []*client.Interface{ + {Name: "iface-a", Summary: "the A iface"}, + {Name: "iface-b", Summary: "the B iface"}, + {Name: "iface-c", Summary: "the C iface"}, + }) +} + +func (cs *clientSuite) TestClientInterfacesConnected(c *check.C) { + // Ask for for a summary of connected interfaces. + cs.rsp = `{ + "type": "sync", + "result": [ + {"name": "iface-a", "summary": "the A iface"}, + {"name": "iface-c", "summary": "the C iface"} + ] + }` + ifaces, err := cs.cli.Interfaces(&client.InterfaceOptions{ + Connected: true, + }) + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + // This uses select=connected to ignore interfaces that just sit on some + // snap but are not connected to anything. + c.Check(cs.req.URL.RawQuery, check.Equals, "select=connected") + c.Assert(err, check.IsNil) + c.Check(ifaces, check.DeepEquals, []*client.Interface{ + {Name: "iface-a", Summary: "the A iface"}, + // interface b was not connected so it doesn't get listed. + {Name: "iface-c", Summary: "the C iface"}, + }) +} + +func (cs *clientSuite) TestClientInterfacesSelectedDetails(c *check.C) { + // Ask for single element and request docs, plugs and slots. + cs.rsp = `{ + "type": "sync", + "result": [ + { + "name": "iface-a", + "summary": "the A iface", + "doc-url": "http://example.org/ifaces/a", + "plugs": [{ + "snap": "consumer", + "plug": "plug", + "interface": "iface-a" + }], + "slots": [{ + "snap": "producer", + "slot": "slot", + "interface": "iface-a" + }] + } + ] + }` + opts := &client.InterfaceOptions{Names: []string{"iface-a"}, Doc: true, Plugs: true, Slots: true} + ifaces, err := cs.cli.Interfaces(opts) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + // This enables documentation, plugs, slots, chooses a specific interface + // (iface-a), and uses select=all to indicate that new response is desired. + c.Check(cs.req.URL.RawQuery, check.Equals, + "doc=true&names=iface-a&plugs=true&select=all&slots=true") + c.Assert(err, check.IsNil) + c.Check(ifaces, check.DeepEquals, []*client.Interface{ + { + Name: "iface-a", + Summary: "the A iface", + DocURL: "http://example.org/ifaces/a", + Plugs: []client.Plug{{Snap: "consumer", Name: "plug", Interface: "iface-a"}}, + Slots: []client.Slot{{Snap: "producer", Name: "slot", Interface: "iface-a"}}, + }, + }) +} + +func (cs *clientSuite) TestClientInterfacesMultiple(c *check.C) { + // Ask for multiple interfaces. + cs.rsp = `{ + "type": "sync", + "result": [ + {"name": "iface-a", "summary": "the A iface"}, + {"name": "iface-b", "summary": "the B iface"} + ] + }` + ifaces, err := cs.cli.Interfaces(&client.InterfaceOptions{Names: []string{"iface-a", "iface-b"}}) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") + // This chooses a specific interfaces (iface-a, iface-b) + c.Check(cs.req.URL.RawQuery, check.Equals, "names=iface-a%2Ciface-b&select=all") + c.Assert(err, check.IsNil) + c.Check(ifaces, check.DeepEquals, []*client.Interface{ + {Name: "iface-a", Summary: "the A iface"}, + {Name: "iface-b", Summary: "the B iface"}, + }) +} + +func (cs *clientSuite) TestClientConnectionsCallsEndpoint(c *check.C) { + _, _ = cs.cli.Connections() + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") +} + +func (cs *clientSuite) TestClientConnections(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": { + "plugs": [ + { + "snap": "canonical-pi2", + "plug": "pin-13", + "interface": "bool-file", + "label": "Pin 13", + "connections": [ + {"snap": "keyboard-lights", "slot": "capslock-led"} + ] + } + ], + "slots": [ + { + "snap": "keyboard-lights", + "slot": "capslock-led", + "interface": "bool-file", + "label": "Capslock indicator LED", + "connections": [ + {"snap": "canonical-pi2", "plug": "pin-13"} + ] + } + ] + } + }` + conns, err := cs.cli.Connections() + c.Assert(err, check.IsNil) + c.Check(conns, check.DeepEquals, client.Connections{ + Plugs: []client.Plug{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.SlotRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }, + Slots: []client.Slot{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "bool-file", + Label: "Capslock indicator LED", + Connections: []client.PlugRef{ + { + Snap: "canonical-pi2", + Name: "pin-13", + }, + }, + }, + }, + }) +} + +func (cs *clientSuite) TestClientConnectCallsEndpoint(c *check.C) { + cs.cli.Connect("producer", "plug", "consumer", "slot") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") +} + +func (cs *clientSuite) TestClientConnect(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "foo" + }` + id, err := cs.cli.Connect("producer", "plug", "consumer", "slot") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "foo") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) +} + +func (cs *clientSuite) TestClientDisconnectCallsEndpoint(c *check.C) { + cs.cli.Disconnect("producer", "plug", "consumer", "slot") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") +} + +func (cs *clientSuite) TestClientDisconnect(c *check.C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "42" + }` + id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot") + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "42") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) +} diff --git a/client/login.go b/client/login.go new file mode 100644 index 00000000..722ccf5d --- /dev/null +++ b/client/login.go @@ -0,0 +1,161 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/snapcore/snapd/osutil" +) + +// User holds logged in user information. +type User struct { + ID int `json:"id,omitempty"` + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + + Macaroon string `json:"macaroon,omitempty"` + Discharges []string `json:"discharges,omitempty"` +} + +type loginData struct { + Email string `json:"email,omitempty"` + Password string `json:"password,omitempty"` + Otp string `json:"otp,omitempty"` +} + +// Login logs user in. +func (client *Client) Login(email, password, otp string) (*User, error) { + postData := loginData{ + Email: email, + Password: password, + Otp: otp, + } + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(postData); err != nil { + return nil, err + } + + var user User + if _, err := client.doSync("POST", "/v2/login", nil, nil, &body, &user); err != nil { + return nil, err + } + + if err := writeAuthData(user); err != nil { + return nil, fmt.Errorf("cannot persist login information: %v", err) + } + return &user, nil +} + +// Logout logs the user out. +func (client *Client) Logout() error { + _, err := client.doSync("POST", "/v2/logout", nil, nil, nil, nil) + if err != nil { + return err + } + return removeAuthData() +} + +// LoggedInUser returns the logged in User or nil +func (client *Client) LoggedInUser() *User { + u, err := readAuthData() + if err != nil { + return nil + } + return u +} + +const authFileEnvKey = "SNAPD_AUTH_DATA_FILENAME" + +func storeAuthDataFilename(homeDir string) string { + if fn := os.Getenv(authFileEnvKey); fn != "" { + return fn + } + + if homeDir == "" { + real, err := osutil.RealUser() + if err != nil { + panic(err) + } + homeDir = real.HomeDir + } + + return filepath.Join(homeDir, ".snap", "auth.json") +} + +// writeAuthData saves authentication details for later reuse through ReadAuthData +func writeAuthData(user User) error { + real, err := osutil.RealUser() + if err != nil { + return err + } + + uid, err := strconv.Atoi(real.Uid) + if err != nil { + return err + } + + gid, err := strconv.Atoi(real.Gid) + if err != nil { + return err + } + + targetFile := storeAuthDataFilename(real.HomeDir) + + if err := osutil.MkdirAllChown(filepath.Dir(targetFile), 0700, uid, gid); err != nil { + return err + } + + outStr, err := json.Marshal(user) + if err != nil { + return nil + } + + return osutil.AtomicWriteFileChown(targetFile, []byte(outStr), 0600, 0, uid, gid) +} + +// readAuthData reads previously written authentication details +func readAuthData() (*User, error) { + sourceFile := storeAuthDataFilename("") + f, err := os.Open(sourceFile) + if err != nil { + return nil, err + } + defer f.Close() + + var user User + dec := json.NewDecoder(f) + if err := dec.Decode(&user); err != nil { + return nil, err + } + + return &user, nil +} + +// removeAuthData removes any previously written authentication details. +func removeAuthData() error { + filename := storeAuthDataFilename("") + return os.Remove(filename) +} diff --git a/client/login_test.go b/client/login_test.go new file mode 100644 index 00000000..c7cc6515 --- /dev/null +++ b/client/login_test.go @@ -0,0 +1,132 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/osutil" +) + +func (cs *clientSuite) TestClientLogin(c *check.C) { + cs.rsp = `{"type": "sync", "result": + {"username": "the-user-name", + "macaroon": "the-root-macaroon", + "discharges": ["discharge-macaroon"]}}` + + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + c.Assert(cs.cli.LoggedInUser(), check.IsNil) + + user, err := cs.cli.Login("username", "pass", "") + c.Check(err, check.IsNil) + c.Check(user, check.DeepEquals, &client.User{ + Username: "the-user-name", + Macaroon: "the-root-macaroon", + Discharges: []string{"discharge-macaroon"}}) + + c.Assert(cs.cli.LoggedInUser(), check.Not(check.IsNil)) + + c.Check(osutil.FileExists(outfile), check.Equals, true) + content, err := ioutil.ReadFile(outfile) + c.Check(err, check.IsNil) + c.Check(string(content), check.Equals, `{"username":"the-user-name","macaroon":"the-root-macaroon","discharges":["discharge-macaroon"]}`) +} + +func (cs *clientSuite) TestClientLoginError(c *check.C) { + cs.rsp = `{ + "result": {}, + "status": "Bad Request", + "status-code": 400, + "type": "error" + }` + + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + user, err := cs.cli.Login("username", "pass", "") + + c.Check(user, check.IsNil) + c.Check(err, check.NotNil) + + c.Check(osutil.FileExists(outfile), check.Equals, false) +} + +func (cs *clientSuite) TestClientLogout(c *check.C) { + cs.rsp = `{"type": "sync", "result": {}}` + + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + err := ioutil.WriteFile(outfile, []byte(`{"macaroon":"macaroon","discharges":["discharged"]}`), 0600) + c.Assert(err, check.IsNil) + + err = cs.cli.Logout() + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/logout")) + + c.Check(osutil.FileExists(outfile), check.Equals, false) +} + +func (cs *clientSuite) TestWriteAuthData(c *check.C) { + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + authData := client.User{ + Macaroon: "macaroon", + Discharges: []string{"discharge"}, + } + err := client.TestWriteAuth(authData) + c.Assert(err, check.IsNil) + + c.Check(osutil.FileExists(outfile), check.Equals, true) + content, err := ioutil.ReadFile(outfile) + c.Check(err, check.IsNil) + c.Check(string(content), check.Equals, `{"macaroon":"macaroon","discharges":["discharge"]}`) +} + +func (cs *clientSuite) TestReadAuthData(c *check.C) { + outfile := filepath.Join(c.MkDir(), "json") + os.Setenv(client.TestAuthFileEnvKey, outfile) + defer os.Unsetenv(client.TestAuthFileEnvKey) + + authData := client.User{ + Macaroon: "macaroon", + Discharges: []string{"discharge"}, + } + err := client.TestWriteAuth(authData) + c.Assert(err, check.IsNil) + + readUser, err := client.TestReadAuth() + c.Assert(err, check.IsNil) + c.Check(readUser, check.DeepEquals, &authData) +} diff --git a/client/packages.go b/client/packages.go new file mode 100644 index 00000000..644467b1 --- /dev/null +++ b/client/packages.go @@ -0,0 +1,233 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + "time" + + "github.com/snapcore/snapd/snap" +) + +// Snap holds the data for a snap as obtained from snapd. +type Snap struct { + ID string `json:"id"` + Title string `json:"title,omitempty"` + Summary string `json:"summary"` + Description string `json:"description"` + DownloadSize int64 `json:"download-size,omitempty"` + Icon string `json:"icon,omitempty"` + InstalledSize int64 `json:"installed-size,omitempty"` + InstallDate time.Time `json:"install-date,omitempty"` + Name string `json:"name"` + Developer string `json:"developer"` + Status string `json:"status"` + Type string `json:"type"` + Version string `json:"version"` + Channel string `json:"channel"` + TrackingChannel string `json:"tracking-channel,omitempty"` + IgnoreValidation bool `json:"ignore-validation"` + Revision snap.Revision `json:"revision"` + Confinement string `json:"confinement"` + Private bool `json:"private"` + DevMode bool `json:"devmode"` + JailMode bool `json:"jailmode"` + TryMode bool `json:"trymode,omitempty"` + Apps []AppInfo `json:"apps,omitempty"` + Broken string `json:"broken,omitempty"` + Contact string `json:"contact"` + License string `json:"license,omitempty"` + + Prices map[string]float64 `json:"prices,omitempty"` + Screenshots []Screenshot `json:"screenshots,omitempty"` + + // The flattended channel map with $track/$risk + Channels map[string]*snap.ChannelSnapInfo `json:"channels,omitempty"` + + // The ordered list of tracks that contains channels + Tracks []string `json:"tracks,omitempty"` +} + +func (s *Snap) MarshalJSON() ([]byte, error) { + type auxSnap Snap // use auxiliary type so that Go does not call Snap.MarshalJSON() + // separate type just for marshalling + m := struct { + auxSnap + InstallDate *time.Time `json:"install-date,omitempty"` + }{ + auxSnap: auxSnap(*s), + } + if !s.InstallDate.IsZero() { + m.InstallDate = &s.InstallDate + } + return json.Marshal(&m) +} + +type Screenshot struct { + URL string `json:"url"` + Width int64 `json:"width,omitempty"` + Height int64 `json:"height,omitempty"` +} + +// Statuses and types a snap may have. +const ( + StatusAvailable = "available" + StatusInstalled = "installed" + StatusActive = "active" + StatusRemoved = "removed" + StatusPriced = "priced" + + TypeApp = "app" + TypeKernel = "kernel" + TypeGadget = "gadget" + TypeOS = "os" + + StrictConfinement = "strict" + DevModeConfinement = "devmode" + ClassicConfinement = "classic" +) + +type ResultInfo struct { + SuggestedCurrency string `json:"suggested-currency"` +} + +// FindOptions supports exactly one of the following options: +// - Refresh: only return snaps that are refreshable +// - Private: return snaps that are private +// - Query: only return snaps that match the query string +type FindOptions struct { + Refresh bool + Private bool + Prefix bool + Query string + Section string +} + +var ErrNoSnapsInstalled = errors.New("no snaps installed") + +type ListOptions struct { + All bool +} + +// List returns the list of all snaps installed on the system +// with names in the given list; if the list is empty, all snaps. +func (client *Client) List(names []string, opts *ListOptions) ([]*Snap, error) { + if opts == nil { + opts = &ListOptions{} + } + + q := make(url.Values) + if opts.All { + q.Add("select", "all") + } + if len(names) > 0 { + q.Add("snaps", strings.Join(names, ",")) + } + + snaps, _, err := client.snapsFromPath("/v2/snaps", q) + if err != nil { + return nil, err + } + + if len(snaps) == 0 { + return nil, ErrNoSnapsInstalled + } + + return snaps, nil +} + +// Sections returns the list of existing snap sections in the store +func (client *Client) Sections() ([]string, error) { + var sections []string + _, err := client.doSync("GET", "/v2/sections", nil, nil, nil, §ions) + if err != nil { + return nil, fmt.Errorf("cannot get snap sections: %s", err) + } + return sections, nil +} + +// Find returns a list of snaps available for install from the +// store for this system and that match the query +func (client *Client) Find(opts *FindOptions) ([]*Snap, *ResultInfo, error) { + if opts == nil { + opts = &FindOptions{} + } + + q := url.Values{} + if opts.Prefix { + q.Set("name", opts.Query+"*") + } else { + q.Set("q", opts.Query) + } + switch { + case opts.Refresh && opts.Private: + return nil, nil, fmt.Errorf("cannot specify refresh and private together") + case opts.Refresh: + q.Set("select", "refresh") + case opts.Private: + q.Set("select", "private") + } + if opts.Section != "" { + q.Set("section", opts.Section) + } + + return client.snapsFromPath("/v2/find", q) +} + +func (client *Client) FindOne(name string) (*Snap, *ResultInfo, error) { + q := url.Values{} + q.Set("name", name) + + snaps, ri, err := client.snapsFromPath("/v2/find", q) + if err != nil { + return nil, nil, fmt.Errorf("cannot find snap %q: %s", name, err) + } + + if len(snaps) == 0 { + return nil, nil, fmt.Errorf("cannot find snap %q", name) + } + + return snaps[0], ri, nil +} + +func (client *Client) snapsFromPath(path string, query url.Values) ([]*Snap, *ResultInfo, error) { + var snaps []*Snap + ri, err := client.doSync("GET", path, query, nil, nil, &snaps) + if err != nil { + return nil, nil, fmt.Errorf("cannot list snaps: %s", err) + } + return snaps, ri, nil +} + +// Snap returns the most recently published revision of the snap with the +// provided name. +func (client *Client) Snap(name string) (*Snap, *ResultInfo, error) { + var snap *Snap + path := fmt.Sprintf("/v2/snaps/%s", name) + ri, err := client.doSync("GET", path, nil, nil, nil, &snap) + if err != nil { + return nil, nil, fmt.Errorf("cannot retrieve snap %q: %s", name, err) + } + return snap, ri, nil +} diff --git a/client/packages_test.go b/client/packages_test.go new file mode 100644 index 00000000..8bf7a8e1 --- /dev/null +++ b/client/packages_test.go @@ -0,0 +1,283 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "encoding/json" + "fmt" + "net/url" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) TestClientSnapsCallsEndpoint(c *check.C) { + _, _ = cs.cli.List(nil, nil) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{}) +} + +func (cs *clientSuite) TestClientFindRefreshSetsQuery(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Refresh: true, + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "q": []string{""}, "select": []string{"refresh"}, + }) +} + +func (cs *clientSuite) TestClientFindRefreshSetsQueryWithSec(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Refresh: true, + Section: "mysection", + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "q": []string{""}, "section": []string{"mysection"}, "select": []string{"refresh"}, + }) +} + +func (cs *clientSuite) TestClientFindWithSectionSetsQuery(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Section: "mysection", + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ + "q": []string{""}, "section": []string{"mysection"}, + }) +} + +func (cs *clientSuite) TestClientFindPrivateSetsQuery(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{ + Private: true, + }) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + + c.Check(cs.req.URL.Query().Get("select"), check.Equals, "private") +} + +func (cs *clientSuite) TestClientSnapsInvalidSnapsJSON(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": "not a list of snaps" + }` + _, err := cs.cli.List(nil, nil) + c.Check(err, check.ErrorMatches, `.*cannot unmarshal.*`) +} + +func (cs *clientSuite) TestClientNoSnaps(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": [], + "suggested-currency": "GBP" + }` + _, err := cs.cli.List(nil, nil) + c.Check(err, check.Equals, client.ErrNoSnapsInstalled) + _, err = cs.cli.List([]string{"foo"}, nil) + c.Check(err, check.Equals, client.ErrNoSnapsInstalled) +} + +func (cs *clientSuite) TestClientSnaps(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": [{ + "id": "funky-snap-id", + "title": "Title", + "summary": "salutation snap", + "description": "hello-world", + "download-size": 22212, + "icon": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", + "installed-size": -1, + "license": "GPL-3.0", + "name": "hello-world", + "developer": "canonical", + "resource": "/v2/snaps/hello-world.canonical", + "status": "available", + "type": "app", + "version": "1.0.18", + "confinement": "strict", + "private": true + }], + "suggested-currency": "GBP" + }` + applications, err := cs.cli.List(nil, nil) + c.Check(err, check.IsNil) + c.Check(applications, check.DeepEquals, []*client.Snap{{ + ID: "funky-snap-id", + Title: "Title", + Summary: "salutation snap", + Description: "hello-world", + DownloadSize: 22212, + Icon: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", + InstalledSize: -1, + License: "GPL-3.0", + Name: "hello-world", + Developer: "canonical", + Status: client.StatusAvailable, + Type: client.TypeApp, + Version: "1.0.18", + Confinement: client.StrictConfinement, + Private: true, + DevMode: false, + }}) +} + +func (cs *clientSuite) TestClientFilterSnaps(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{Query: "foo"}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.RawQuery, check.Equals, "q=foo") +} + +func (cs *clientSuite) TestClientFindPrefix(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{Query: "foo", Prefix: true}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo%2A") // 2A is `*` +} + +func (cs *clientSuite) TestClientFindOne(c *check.C) { + _, _, _ = cs.cli.FindOne("foo") + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo") +} + +const ( + pkgName = "chatroom" +) + +func (cs *clientSuite) TestClientSnap(c *check.C) { + // example data obtained via + // printf "GET /v2/find?name=test-snapd-tools HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket|grep '{'|python3 -m json.tool + cs.rsp = `{ + "type": "sync", + "result": { + "id": "funky-snap-id", + "title": "Title", + "summary": "bla bla", + "description": "WebRTC Video chat server for Snappy", + "download-size": 6930947, + "icon": "/v2/icons/chatroom.ogra/icon", + "installed-size": 18976651, + "install-date": "2016-01-02T15:04:05Z", + "license": "GPL-3.0", + "name": "chatroom", + "developer": "ogra", + "resource": "/v2/snaps/chatroom.ogra", + "status": "active", + "type": "app", + "version": "0.1-8", + "confinement": "strict", + "private": true, + "devmode": true, + "trymode": true, + "screenshots": [ + {"url":"http://example.com/shot1.png", "width":640, "height":480}, + {"url":"http://example.com/shot2.png"} + ] + } + }` + pkg, _, err := cs.cli.Snap(pkgName) + c.Assert(cs.req.Method, check.Equals, "GET") + c.Assert(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName)) + c.Assert(err, check.IsNil) + c.Assert(pkg, check.DeepEquals, &client.Snap{ + ID: "funky-snap-id", + Summary: "bla bla", + Title: "Title", + Description: "WebRTC Video chat server for Snappy", + DownloadSize: 6930947, + Icon: "/v2/icons/chatroom.ogra/icon", + InstalledSize: 18976651, + InstallDate: time.Date(2016, 1, 2, 15, 4, 5, 0, time.UTC), + License: "GPL-3.0", + Name: "chatroom", + Developer: "ogra", + Status: client.StatusActive, + Type: client.TypeApp, + Version: "0.1-8", + Confinement: client.StrictConfinement, + Private: true, + DevMode: true, + TryMode: true, + Screenshots: []client.Screenshot{ + {URL: "http://example.com/shot1.png", Width: 640, Height: 480}, + {URL: "http://example.com/shot2.png"}, + }, + }) +} + +func (cs *clientSuite) TestAppInfoNoServiceNoDaemon(c *check.C) { + buf, err := json.MarshalIndent(client.AppInfo{Name: "hello"}, "\t", "\t") + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Equals, `{ + "name": "hello" + }`) +} + +func (cs *clientSuite) TestAppInfoServiceDaemon(c *check.C) { + buf, err := json.MarshalIndent(client.AppInfo{ + Snap: "foo", + Name: "hello", + Daemon: "daemon", + Enabled: true, + Active: false, + }, "\t", "\t") + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Equals, `{ + "snap": "foo", + "name": "hello", + "daemon": "daemon", + "enabled": true + }`) +} + +func (cs *clientSuite) TestAppInfoNilNotService(c *check.C) { + var app *client.AppInfo + c.Check(app.IsService(), check.Equals, false) +} + +func (cs *clientSuite) TestAppInfoNoDaemonNotService(c *check.C) { + var app *client.AppInfo + c.Assert(json.Unmarshal([]byte(`{"name": "hello"}`), &app), check.IsNil) + c.Check(app.Name, check.Equals, "hello") + c.Check(app.IsService(), check.Equals, false) +} + +func (cs *clientSuite) TestAppInfoEmptyDaemonNotService(c *check.C) { + var app *client.AppInfo + c.Assert(json.Unmarshal([]byte(`{"name": "hello", "daemon": ""}`), &app), check.IsNil) + c.Check(app.Name, check.Equals, "hello") + c.Check(app.IsService(), check.Equals, false) +} + +func (cs *clientSuite) TestAppInfoDaemonIsService(c *check.C) { + var app *client.AppInfo + + c.Assert(json.Unmarshal([]byte(`{"name": "hello", "daemon": "x"}`), &app), check.IsNil) + c.Check(app.Name, check.Equals, "hello") + c.Check(app.IsService(), check.Equals, true) +} diff --git a/client/snap_op.go b/client/snap_op.go new file mode 100644 index 00000000..75d5aeea --- /dev/null +++ b/client/snap_op.go @@ -0,0 +1,260 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" +) + +type SnapOptions struct { + Channel string `json:"channel,omitempty"` + Revision string `json:"revision,omitempty"` + DevMode bool `json:"devmode,omitempty"` + JailMode bool `json:"jailmode,omitempty"` + Classic bool `json:"classic,omitempty"` + Dangerous bool `json:"dangerous,omitempty"` + IgnoreValidation bool `json:"ignore-validation,omitempty"` + Unaliased bool `json:"unaliased,omitempty"` +} + +func (opts *SnapOptions) writeModeFields(mw *multipart.Writer) error { + fields := []struct { + f string + b bool + }{ + {"devmode", opts.DevMode}, + {"classic", opts.Classic}, + {"jailmode", opts.JailMode}, + {"dangerous", opts.Dangerous}, + } + for _, o := range fields { + if !o.b { + continue + } + err := mw.WriteField(o.f, "true") + if err != nil { + return err + } + } + + return nil +} + +type actionData struct { + Action string `json:"action"` + Name string `json:"name,omitempty"` + SnapPath string `json:"snap-path,omitempty"` + *SnapOptions +} + +type multiActionData struct { + Action string `json:"action"` + Snaps []string `json:"snaps,omitempty"` +} + +// Install adds the snap with the given name from the given channel (or +// the system default channel if not). +func (client *Client) Install(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("install", name, options) +} + +func (client *Client) InstallMany(names []string, options *SnapOptions) (changeID string, err error) { + return client.doMultiSnapAction("install", names, options) +} + +// Remove removes the snap with the given name. +func (client *Client) Remove(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("remove", name, options) +} + +func (client *Client) RemoveMany(names []string, options *SnapOptions) (changeID string, err error) { + return client.doMultiSnapAction("remove", names, options) +} + +// Refresh refreshes the snap with the given name (switching it to track +// the given channel if given). +func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("refresh", name, options) +} + +func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) { + return client.doMultiSnapAction("refresh", names, options) +} + +func (client *Client) Enable(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("enable", name, options) +} + +func (client *Client) Disable(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("disable", name, options) +} + +// Revert rolls the snap back to the previous on-disk state +func (client *Client) Revert(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("revert", name, options) +} + +// Switch moves the snap to a different channel without a refresh +func (client *Client) Switch(name string, options *SnapOptions) (changeID string, err error) { + return client.doSnapAction("switch", name, options) +} + +var ErrDangerousNotApplicable = fmt.Errorf("dangerous option only meaningful when installing from a local file") + +func (client *Client) doSnapAction(actionName string, snapName string, options *SnapOptions) (changeID string, err error) { + if options != nil && options.Dangerous { + return "", ErrDangerousNotApplicable + } + action := actionData{ + Action: actionName, + SnapOptions: options, + } + data, err := json.Marshal(&action) + if err != nil { + return "", fmt.Errorf("cannot marshal snap action: %s", err) + } + path := fmt.Sprintf("/v2/snaps/%s", snapName) + + headers := map[string]string{ + "Content-Type": "application/json", + } + + return client.doAsync("POST", path, nil, headers, bytes.NewBuffer(data)) +} + +func (client *Client) doMultiSnapAction(actionName string, snaps []string, options *SnapOptions) (changeID string, err error) { + if options != nil { + return "", fmt.Errorf("cannot use options for multi-action") // (yet) + } + action := multiActionData{ + Action: actionName, + Snaps: snaps, + } + data, err := json.Marshal(&action) + if err != nil { + return "", fmt.Errorf("cannot marshal multi-snap action: %s", err) + } + + headers := map[string]string{ + "Content-Type": "application/json", + } + + return client.doAsync("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data)) +} + +// InstallPath sideloads the snap with the given path, returning the UUID +// of the background operation upon success. +func (client *Client) InstallPath(path string, options *SnapOptions) (changeID string, err error) { + f, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("cannot open: %q", path) + } + + action := actionData{ + Action: "install", + SnapPath: path, + SnapOptions: options, + } + + pr, pw := io.Pipe() + mw := multipart.NewWriter(pw) + go sendSnapFile(path, f, pw, mw, &action) + + headers := map[string]string{ + "Content-Type": mw.FormDataContentType(), + } + + return client.doAsync("POST", "/v2/snaps", nil, headers, pr) +} + +// Try +func (client *Client) Try(path string, options *SnapOptions) (changeID string, err error) { + if options == nil { + options = &SnapOptions{} + } + if options.Dangerous { + return "", ErrDangerousNotApplicable + } + + buf := bytes.NewBuffer(nil) + mw := multipart.NewWriter(buf) + mw.WriteField("action", "try") + mw.WriteField("snap-path", path) + options.writeModeFields(mw) + mw.Close() + + headers := map[string]string{ + "Content-Type": mw.FormDataContentType(), + } + + return client.doAsync("POST", "/v2/snaps", nil, headers, buf) +} + +func sendSnapFile(snapPath string, snapFile *os.File, pw *io.PipeWriter, mw *multipart.Writer, action *actionData) { + defer snapFile.Close() + + if action.SnapOptions == nil { + action.SnapOptions = &SnapOptions{} + } + fields := []struct { + name string + value string + }{ + {"action", action.Action}, + {"name", action.Name}, + {"snap-path", action.SnapPath}, + {"channel", action.Channel}, + } + for _, s := range fields { + if s.value == "" { + continue + } + if err := mw.WriteField(s.name, s.value); err != nil { + pw.CloseWithError(err) + return + } + } + + if err := action.writeModeFields(mw); err != nil { + pw.CloseWithError(err) + return + } + + fw, err := mw.CreateFormFile("snap", filepath.Base(snapPath)) + if err != nil { + pw.CloseWithError(err) + return + } + + _, err = io.Copy(fw, snapFile) + if err != nil { + pw.CloseWithError(err) + return + } + + mw.Close() + pw.Close() +} diff --git a/client/snap_op_test.go b/client/snap_op_test.go new file mode 100644 index 00000000..62938670 --- /dev/null +++ b/client/snap_op_test.go @@ -0,0 +1,309 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "path/filepath" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +var chanName = "achan" + +var ops = []struct { + op func(*client.Client, string, *client.SnapOptions) (string, error) + action string +}{ + {(*client.Client).Install, "install"}, + {(*client.Client).Refresh, "refresh"}, + {(*client.Client).Remove, "remove"}, + {(*client.Client).Revert, "revert"}, + {(*client.Client).Enable, "enable"}, + {(*client.Client).Disable, "disable"}, + {(*client.Client).Switch, "switch"}, +} + +var multiOps = []struct { + op func(*client.Client, []string, *client.SnapOptions) (string, error) + action string +}{ + {(*client.Client).RefreshMany, "refresh"}, + {(*client.Client).InstallMany, "install"}, + {(*client.Client).RemoveMany, "remove"}, +} + +func (cs *clientSuite) TestClientOpSnapServerError(c *check.C) { + cs.err = errors.New("fail") + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientMultiOpSnapServerError(c *check.C) { + cs.err = errors.New("fail") + for _, s := range multiOps { + _, err := s.op(cs.cli, nil, nil) + c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) { + cs.rsp = `{"type": "error", "status": "potatoes"}` + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientMultiOpSnapResponseError(c *check.C) { + cs.rsp = `{"type": "error", "status": "potatoes"}` + for _, s := range multiOps { + _, err := s.op(cs.cli, nil, nil) + c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) { + cs.rsp = `{"type": "what"}` + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Check(err, check.ErrorMatches, `.*expected async response for "POST" on "/v2/snaps/`+pkgName+`", got "what"`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnapNotAccepted(c *check.C) { + cs.rsp = `{ + "status-code": 200, + "type": "async" + }` + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Check(err, check.ErrorMatches, `.*operation not accepted`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) { + cs.rsp = `{ + "status-code": 202, + "type": "async" + }` + for _, s := range ops { + _, err := s.op(cs.cli, pkgName, nil) + c.Assert(err, check.ErrorMatches, `.*response without change reference.*`, check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpSnap(c *check.C) { + cs.rsp = `{ + "change": "d728", + "status-code": 202, + "type": "async" + }` + for _, s := range ops { + id, err := s.op(cs.cli, pkgName, &client.SnapOptions{Channel: chanName}) + c.Assert(err, check.IsNil) + + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil, check.Commentf(s.action)) + jsonBody := make(map[string]string) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil, check.Commentf(s.action)) + c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action)) + c.Check(jsonBody["channel"], check.Equals, chanName, check.Commentf(s.action)) + c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action)) + + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName), check.Commentf(s.action)) + c.Check(id, check.Equals, "d728", check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) { + cs.rsp = `{ + "change": "d728", + "status-code": 202, + "type": "async" + }` + for _, s := range multiOps { + id, err := s.op(cs.cli, []string{pkgName}, nil) + c.Assert(err, check.IsNil) + + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil, check.Commentf(s.action)) + jsonBody := make(map[string]interface{}) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil, check.Commentf(s.action)) + c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action)) + c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}, check.Commentf(s.action)) + c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action)) + + c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps", check.Commentf(s.action)) + c.Check(id, check.Equals, "d728", check.Commentf(s.action)) + } +} + +func (cs *clientSuite) TestClientOpInstallPath(c *check.C) { + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + bodyData := []byte("snap-data") + + snap := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snap, bodyData, 0644) + c.Assert(err, check.IsNil) + + id, err := cs.cli.InstallPath(snap, nil) + c.Assert(err, check.IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") + + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) + c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") + c.Check(id, check.Equals, "66b3") +} + +func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) { + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + bodyData := []byte("snap-data") + + snap := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snap, bodyData, 0644) + c.Assert(err, check.IsNil) + + opts := client.SnapOptions{ + Dangerous: true, + } + + // InstallPath takes Dangerous + _, err = cs.cli.InstallPath(snap, &opts) + c.Assert(err, check.IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*") + + // Install does not (and gives us a clear error message) + _, err = cs.cli.Install("foo", &opts) + c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) + + // nor does InstallMany (whether it fails because any option + // at all was provided, or because dangerous was provided, is + // unimportant) + _, err = cs.cli.InstallMany([]string{"foo"}, &opts) + c.Assert(err, check.NotNil) +} + +func formToMap(c *check.C, mr *multipart.Reader) map[string]string { + formData := map[string]string{} + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } + c.Assert(err, check.IsNil) + slurp, err := ioutil.ReadAll(p) + c.Assert(err, check.IsNil) + formData[p.FormName()] = string(slurp) + } + return formData +} + +func (cs *clientSuite) TestClientOpTryMode(c *check.C) { + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + snapdir := filepath.Join(c.MkDir(), "/some/path") + + for _, opts := range []*client.SnapOptions{ + {Classic: false, DevMode: false, JailMode: false}, + {Classic: false, DevMode: false, JailMode: true}, + {Classic: false, DevMode: true, JailMode: true}, + {Classic: false, DevMode: true, JailMode: false}, + {Classic: true, DevMode: false, JailMode: false}, + {Classic: true, DevMode: false, JailMode: true}, + {Classic: true, DevMode: true, JailMode: true}, + {Classic: true, DevMode: true, JailMode: false}, + } { + comment := check.Commentf("when Classic:%t DevMode:%t JailMode:%t", opts.Classic, opts.DevMode, opts.JailMode) + id, err := cs.cli.Try(snapdir, opts) + c.Assert(err, check.IsNil) + + // ensure we send the right form-data + _, params, err := mime.ParseMediaType(cs.req.Header.Get("Content-Type")) + c.Assert(err, check.IsNil, comment) + mr := multipart.NewReader(cs.req.Body, params["boundary"]) + formData := formToMap(c, mr) + c.Check(formData["action"], check.Equals, "try", comment) + c.Check(formData["snap-path"], check.Equals, snapdir, comment) + expectedLength := 2 + if opts.Classic { + c.Check(formData["classic"], check.Equals, "true", comment) + expectedLength++ + } + if opts.DevMode { + c.Check(formData["devmode"], check.Equals, "true", comment) + expectedLength++ + } + if opts.JailMode { + c.Check(formData["jailmode"], check.Equals, "true", comment) + expectedLength++ + } + c.Check(len(formData), check.Equals, expectedLength) + + c.Check(cs.req.Method, check.Equals, "POST", comment) + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"), comment) + c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*", comment) + c.Check(id, check.Equals, "66b3", comment) + } +} + +func (cs *clientSuite) TestClientOpTryModeDangerous(c *check.C) { + snapdir := filepath.Join(c.MkDir(), "/some/path") + + _, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true}) + c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) +} diff --git a/client/snapctl.go b/client/snapctl.go new file mode 100644 index 00000000..ae36e18f --- /dev/null +++ b/client/snapctl.go @@ -0,0 +1,57 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// SnapCtlOptions holds the various options with which snapctl is invoked. +type SnapCtlOptions struct { + // ContextID is a string used to determine the context of this call (e.g. + // which context and handler should be used, etc.) + ContextID string `json:"context-id"` + + // Args contains a list of parameters to use for this invocation. + Args []string `json:"args"` +} + +type snapctlOutput struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +// RunSnapctl requests a snapctl run for the given options. +func (client *Client) RunSnapctl(options *SnapCtlOptions) (stdout, stderr []byte, err error) { + b, err := json.Marshal(options) + if err != nil { + return nil, nil, fmt.Errorf("cannot marshal options: %s", err) + } + + var output snapctlOutput + _, err = client.doSync("POST", "/v2/snapctl", nil, nil, bytes.NewReader(b), &output) + if err != nil { + return nil, nil, err + } + + return []byte(output.Stdout), []byte(output.Stderr), nil +} diff --git a/client/snapctl_test.go b/client/snapctl_test.go new file mode 100644 index 00000000..9ddb1a81 --- /dev/null +++ b/client/snapctl_test.go @@ -0,0 +1,68 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package client_test + +import ( + "encoding/json" + + "github.com/snapcore/snapd/client" + + "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientRunSnapctlCallsEndpoint(c *check.C) { + options := &client.SnapCtlOptions{ + ContextID: "1234ABCD", + Args: []string{"foo", "bar"}, + } + cs.cli.RunSnapctl(options) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/snapctl") +} + +func (cs *clientSuite) TestClientRunSnapctl(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { + "stdout": "test stdout", + "stderr": "test stderr" + } + }` + + options := &client.SnapCtlOptions{ + ContextID: "1234ABCD", + Args: []string{"foo", "bar"}, + } + + stdout, stderr, err := cs.cli.RunSnapctl(options) + c.Assert(err, check.IsNil) + c.Check(string(stdout), check.Equals, "test stdout") + c.Check(string(stderr), check.Equals, "test stderr") + + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "context-id": "1234ABCD", + "args": []interface{}{"foo", "bar"}, + }) +} diff --git a/cmd/.indent.pro b/cmd/.indent.pro new file mode 100644 index 00000000..f410d3c6 --- /dev/null +++ b/cmd/.indent.pro @@ -0,0 +1,34 @@ +-nbad +-bap +-nbc +-bbo +-hnl +-br +-brs +-c33 +-cd33 +-ncdb +-ce +-ci4 +-cli0 +-d0 +-di1 +-nfc1 +-i8 +-ip0 +-l80 +-lp +-npcs +-nprs +-npsl +-sai +-saf +-saw +-ncs +-nsc +-sob +-nfca +-cp33 +-ss +-ts8 +-il1 diff --git a/cmd/Makefile.am b/cmd/Makefile.am new file mode 100644 index 00000000..9063a16b --- /dev/null +++ b/cmd/Makefile.am @@ -0,0 +1,424 @@ + +EXTRA_DIST = VERSION snap-confine/PORTING +CLEANFILES = +TESTS = +libexec_PROGRAMS = +dist_man_MANS = +noinst_PROGRAMS = +noinst_LIBRARIES = + +CHECK_CFLAGS = -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes \ + -Wno-missing-field-initializers -Wno-unused-parameter + +# Make all warnings errors when building for unit tests +if WITH_UNIT_TESTS +CHECK_CFLAGS += -Werror +endif + +subdirs = snap-confine snap-discard-ns system-shutdown libsnap-confine-private + +# Run check-syntax when checking +# TODO: conver those to autotools-style tests later +check: check-syntax-c check-unit-tests + +# Force particular coding style on all source and header files. +.PHONY: check-syntax-c +check-syntax-c: + @d=`mktemp -d`; \ + trap 'rm -rf $d' EXIT; \ + for f in $(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])) ; do \ + out="$$d/`basename $$f.out`"; \ + echo "Checking $$f ... "; \ + HOME=$(srcdir) indent "$$f" -o "$$out"; \ + diff -Naur "$$f" "$$out" || exit 1; \ + done; + +.PHONY: check-unit-tests +if WITH_UNIT_TESTS +check-unit-tests: snap-confine/unit-tests system-shutdown/unit-tests libsnap-confine-private/unit-tests + $(HAVE_VALGRIND) ./libsnap-confine-private/unit-tests + $(HAVE_VALGRIND) ./snap-confine/unit-tests + $(HAVE_VALGRIND) ./system-shutdown/unit-tests +else +check-unit-tests: + echo "unit tests are disabled (rebuild with --enable-unit-tests)" +endif + +.PHONY: fmt +fmt: $(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])) + HOME=$(srcdir) indent $^ + +# The hack target helps devlopers work on snap-confine on their live system by +# installing a fresh copy of snap confine and the appropriate apparmor profile. +.PHONY: hack +hack: snap-confine/snap-confine snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp + sudo install -D -m 6755 snap-confine/snap-confine $(DESTDIR)$(libexecdir)/snap-confine + sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real + sudo install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/ + sudo apparmor_parser -r snap-confine/snap-confine.apparmor + sudo install -m 755 snap-update-ns/snap-update-ns $(DESTDIR)$(libexecdir)/snap-update-ns + sudo install -m 755 snap-seccomp/snap-seccomp $(DESTDIR)$(libexecdir)/snap-seccomp + +# for the hack target also: +snap-update-ns/snap-update-ns: snap-update-ns/*.go snap-update-ns/*.[ch] + cd snap-update-ns && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -i -v +snap-seccomp/snap-seccomp: snap-seccomp/*.go + cd snap-seccomp && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -i -v + +## +## libsnap-confine-private.a +## + +noinst_LIBRARIES += libsnap-confine-private.a + +libsnap_confine_private_a_SOURCES = \ + libsnap-confine-private/cgroup-freezer-support.c \ + libsnap-confine-private/cgroup-freezer-support.h \ + libsnap-confine-private/classic.c \ + libsnap-confine-private/classic.h \ + libsnap-confine-private/cleanup-funcs.c \ + libsnap-confine-private/cleanup-funcs.h \ + libsnap-confine-private/error.c \ + libsnap-confine-private/error.h \ + libsnap-confine-private/fault-injection.c \ + libsnap-confine-private/fault-injection.h \ + libsnap-confine-private/locking.c \ + libsnap-confine-private/locking.h \ + libsnap-confine-private/mount-opt.c \ + libsnap-confine-private/mount-opt.h \ + libsnap-confine-private/mountinfo.c \ + libsnap-confine-private/mountinfo.h \ + libsnap-confine-private/privs.c \ + libsnap-confine-private/privs.h \ + libsnap-confine-private/secure-getenv.c \ + libsnap-confine-private/secure-getenv.h \ + libsnap-confine-private/snap.c \ + libsnap-confine-private/snap.h \ + libsnap-confine-private/string-utils.c \ + libsnap-confine-private/string-utils.h \ + libsnap-confine-private/utils.c \ + libsnap-confine-private/utils.h +libsnap_confine_private_a_CFLAGS = $(CHECK_CFLAGS) + +if WITH_UNIT_TESTS +noinst_PROGRAMS += libsnap-confine-private/unit-tests +libsnap_confine_private_unit_tests_SOURCES = \ + libsnap-confine-private/classic-test.c \ + libsnap-confine-private/cleanup-funcs-test.c \ + libsnap-confine-private/error-test.c \ + libsnap-confine-private/fault-injection-test.c \ + libsnap-confine-private/locking-test.c \ + libsnap-confine-private/mount-opt-test.c \ + libsnap-confine-private/mountinfo-test.c \ + libsnap-confine-private/privs-test.c \ + libsnap-confine-private/secure-getenv-test.c \ + libsnap-confine-private/snap-test.c \ + libsnap-confine-private/string-utils-test.c \ + libsnap-confine-private/test-utils.c \ + libsnap-confine-private/test-utils-test.c \ + libsnap-confine-private/unit-tests-main.c \ + libsnap-confine-private/unit-tests.c \ + libsnap-confine-private/unit-tests.h \ + libsnap-confine-private/utils-test.c +libsnap_confine_private_unit_tests_CFLAGS = $(CHECK_CFLAGS) $(GLIB_CFLAGS) +libsnap_confine_private_unit_tests_LDADD = $(GLIB_LIBS) +libsnap_confine_private_unit_tests_CFLAGS += -D_ENABLE_FAULT_INJECTION +libsnap_confine_private_unit_tests_STATIC = + +if STATIC_LIBCAP +libsnap_confine_private_unit_tests_STATIC += -lcap +else +libsnap_confine_private_unit_tests_LDADD += -lcap +endif # STATIC_LIBCAP + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +libsnap-confine-private/unit-tests$(EXEEXT): $(libsnap_confine_private_unit_tests_OBJECTS) $(libsnap_confine_private_unit_tests_DEPENDENCIES) $(EXTRA_libsnap_confine_private_unit_tests_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f libsnap-confine-private/unit-tests$(EXEEXT) + $(AM_V_CCLD)$(libsnap_confine_private_unit_tests_LINK) $(libsnap_confine_private_unit_tests_OBJECTS) $(libsnap_confine_private_unit_tests_LDADD) $(LIBS) + +libsnap-confine-private/unit-tests$(EXEEXT): LIBS += -Wl,-Bstatic $(libsnap_confine_private_unit_tests_STATIC) -Wl,-Bdynamic +endif # WITH_UNIT_TESTS + +## +## decode-mount-opts +## + +noinst_PROGRAMS += decode-mount-opts/decode-mount-opts + +decode_mount_opts_decode_mount_opts_SOURCES = \ + decode-mount-opts/decode-mount-opts.c +decode_mount_opts_decode_mount_opts_LDADD = libsnap-confine-private.a +decode_mount_opts_decode_mount_opts_STATIC = + +if STATIC_LIBCAP +decode_mount_opts_decode_mount_opts_STATIC += -lcap +else +decode_mount_opts_decode_mount_opts_LDADD += -lcap +endif # STATIC_LIBCAP + +# XXX: this makes automake generate decode_mount_opts_decode_mount_opts_LINK +decode_mount_opts_decode_mount_opts_CFLAGS = -D_fake + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +decode-mount-opts/decode-mount-opts$(EXEEXT): $(decode_mount_opts_decode_mount_opts_OBJECTS) $(decode_mount_opts_decode_mount_opts_DEPENDENCIES) $(EXTRA_decode_mount_opts_decode_mount_opts_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f decode-mount-opts/decode-mount-opts$(EXEEXT) + $(AM_V_CCLD)$(decode_mount_opts_decode_mount_opts_LINK) $(decode_mount_opts_decode_mount_opts_OBJECTS) $(decode_mount_opts_decode_mount_opts_LDADD) $(LIBS) + +decode-mount-opts/decode-mount-opts$(EXEEXT): LIBS += -Wl,-Bstatic $(decode_mount_opts_decode_mount_opts_STATIC) -Wl,-Bdynamic + +## +## snap-confine +## + +libexec_PROGRAMS += snap-confine/snap-confine +if HAVE_RST2MAN +dist_man_MANS += snap-confine/snap-confine.1 +CLEANFILES += snap-confine/snap-confine.1 +endif +EXTRA_DIST += snap-confine/snap-confine.rst +EXTRA_DIST += snap-confine/snap-confine.apparmor.in + +snap_confine_snap_confine_SOURCES = \ + snap-confine/apparmor-support.c \ + snap-confine/apparmor-support.h \ + snap-confine/cookie-support.c \ + snap-confine/cookie-support.h \ + snap-confine/mount-support-nvidia.c \ + snap-confine/mount-support-nvidia.h \ + snap-confine/mount-support.c \ + snap-confine/mount-support.h \ + snap-confine/ns-support.c \ + snap-confine/ns-support.h \ + snap-confine/quirks.c \ + snap-confine/quirks.h \ + snap-confine/snap-confine-args.c \ + snap-confine/snap-confine-args.h \ + snap-confine/snap-confine.c \ + snap-confine/udev-support.c \ + snap-confine/udev-support.h \ + snap-confine/user-support.c \ + snap-confine/user-support.h + +snap_confine_snap_confine_CFLAGS = $(CHECK_CFLAGS) $(AM_CFLAGS) -DLIBEXECDIR=\"$(libexecdir)\" +snap_confine_snap_confine_LDFLAGS = $(AM_LDFLAGS) +snap_confine_snap_confine_LDADD = libsnap-confine-private.a +snap_confine_snap_confine_CFLAGS += $(LIBUDEV_CFLAGS) +snap_confine_snap_confine_LDADD += $(LIBUDEV_LIBS) +# _STATIC is where we collect statically linked in libraries +snap_confine_snap_confine_STATIC = + +if STATIC_LIBCAP +snap_confine_snap_confine_STATIC += -lcap +else +snap_confine_snap_confine_LDADD += -lcap +endif # STATIC_LIBCAP + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +snap-confine/snap-confine$(EXEEXT): $(snap_confine_snap_confine_OBJECTS) $(snap_confine_snap_confine_DEPENDENCIES) $(EXTRA_snap_confine_snap_confine_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/snap-confine$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_snap_confine_LINK) $(snap_confine_snap_confine_OBJECTS) $(snap_confine_snap_confine_LDADD) $(LIBS) + +snap-confine/snap-confine$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_confine_snap_confine_STATIC) -Wl,-Bdynamic -pthread + +# This is here to help fix rpmlint hardening issue. +# https://en.opensuse.org/openSUSE:Packaging_checks#non-position-independent-executable +snap_confine_snap_confine_CFLAGS += $(SUID_CFLAGS) +snap_confine_snap_confine_LDFLAGS += $(SUID_LDFLAGS) + +if SECCOMP +snap_confine_snap_confine_SOURCES += \ + snap-confine/seccomp-support.c \ + snap-confine/seccomp-support.h +snap_confine_snap_confine_CFLAGS += $(SECCOMP_CFLAGS) +if STATIC_LIBSECCOMP +snap_confine_snap_confine_STATIC += $(shell pkg-config --static --libs libseccomp) +else +snap_confine_snap_confine_LDADD += $(SECCOMP_LIBS) +endif # STATIC_LIBSECCOMP +endif # SECCOMP + +if APPARMOR +snap_confine_snap_confine_CFLAGS += $(APPARMOR_CFLAGS) +if STATIC_LIBAPPARMOR +snap_confine_snap_confine_STATIC += $(shell pkg-config --static --libs libapparmor) +else +snap_confine_snap_confine_LDADD += $(APPARMOR_LIBS) +endif # STATIC_LIBAPPARMOR +endif # APPARMOR + +# an extra build that has additional debugging enabled at compile time + +noinst_PROGRAMS += snap-confine/snap-confine-debug +snap_confine_snap_confine_debug_SOURCES = $(snap_confine_snap_confine_SOURCES) +snap_confine_snap_confine_debug_CFLAGS = $(snap_confine_snap_confine_CFLAGS) +snap_confine_snap_confine_debug_LDFLAGS = $(snap_confine_snap_confine_LDFLAGS) +snap_confine_snap_confine_debug_LDADD = $(snap_confine_snap_confine_LDADD) +snap_confine_snap_confine_debug_CFLAGS += -DSNAP_CONFINE_DEBUG_BUILD=1 +snap_confine_snap_confine_debug_STATIC = $(snap_confine_snap_confine_STATIC) + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +snap-confine/snap-confine-debug$(EXEEXT): $(snap_confine_snap_confine_debug_OBJECTS) $(snap_confine_snap_confine_debug_DEPENDENCIES) $(EXTRA_snap_confine_snap_confine_debug_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/snap-confine-debug$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_snap_confine_debug_LINK) $(snap_confine_snap_confine_debug_OBJECTS) $(snap_confine_snap_confine_debug_LDADD) $(LIBS) + +snap-confine/snap-confine-debug$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_confine_snap_confine_debug_STATIC) -Wl,-Bdynamic -pthread + +if WITH_UNIT_TESTS +noinst_PROGRAMS += snap-confine/unit-tests +snap_confine_unit_tests_SOURCES = \ + libsnap-confine-private/test-utils.c \ + libsnap-confine-private/unit-tests-main.c \ + libsnap-confine-private/unit-tests.c \ + libsnap-confine-private/unit-tests.h \ + snap-confine/apparmor-support.c \ + snap-confine/apparmor-support.h \ + snap-confine/cookie-support-test.c \ + snap-confine/mount-support-test.c \ + snap-confine/ns-support-test.c \ + snap-confine/quirks.c \ + snap-confine/quirks.h \ + snap-confine/snap-confine-args-test.c +snap_confine_unit_tests_CFLAGS = $(snap_confine_snap_confine_CFLAGS) $(GLIB_CFLAGS) +snap_confine_unit_tests_LDADD = $(snap_confine_snap_confine_LDADD) $(GLIB_LIBS) +snap_confine_unit_tests_LDFLAGS = $(snap_confine_snap_confine_LDFLAGS) +snap_confine_unit_tests_STATIC = $(snap_confine_snap_confine_STATIC) + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +snap-confine/unit-tests$(EXEEXT): $(snap_confine_unit_tests_OBJECTS) $(snap_confine_unit_tests_DEPENDENCIES) $(EXTRA_snap_confine_unit_tests_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/unit-tests$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_unit_tests_LINK) $(snap_confine_unit_tests_OBJECTS) $(snap_confine_unit_tests_LDADD) $(LIBS) + +snap-confine/unit-tests$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_confine_unit_tests_STATIC) -Wl,-Bdynamic -pthread +endif # WITH_UNIT_TESTS + +if HAVE_RST2MAN +snap-confine/%.1: snap-confine/%.rst + mkdir -p snap-confine + $(HAVE_RST2MAN) $^ > $@ +endif + +snap-confine/snap-confine.apparmor: snap-confine/snap-confine.apparmor.in Makefile + sed -e 's,[@]LIBEXECDIR[@],$(libexecdir),g' -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@ + +# Install the apparmor profile +# +# NOTE: the funky make functions here just convert /foo/bar/froz into +# foo.bar.froz The inner subst replaces slashes with dots and the outer +# patsubst strips the leading dot +install-data-local:: snap-confine/snap-confine.apparmor +if APPARMOR + install -d -m 755 $(DESTDIR)/etc/apparmor.d/ + install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine +endif + install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/ + +# NOTE: The 'void' directory *has to* be chmod 000 +install-data-local:: + install -d -m 000 $(DESTDIR)/var/lib/snapd/void + +install-exec-hook:: +if CAPS_OVER_SETUID +# Ensure that snap-confine has CAP_SYS_ADMIN capability + setcap cap_sys_admin=pe $(DESTDIR)$(libexecdir)/snap-confine +else +# Ensure that snap-confine is u+s,g+s (setuid and setgid) + chmod 6755 $(DESTDIR)$(libexecdir)/snap-confine +endif + +## +## ubuntu-core-launcher +## + +install-exec-hook:: + install -d -m 755 $(DESTDIR)$(bindir) + ln -sf $(libexecdir)/snap-confine $(DESTDIR)$(bindir)/ubuntu-core-launcher + +## +## snappy-app-dev +## + +EXTRA_DIST += \ + snap-confine/snappy-app-dev + +# NOTE: This makes distcheck fail but it is required for udev, so go figure. +# http://www.gnu.org/software/automake/manual/automake.html#Hard_002dCoded-Install-Paths +# +# Install support script for udev rules +install-exec-local:: + install -d -m 755 $(DESTDIR)$(shell pkg-config udev --variable=udevdir) + install -m 755 $(srcdir)/snap-confine/snappy-app-dev $(DESTDIR)$(shell pkg-config udev --variable=udevdir) + +## +## snap-discard-ns +## + +libexec_PROGRAMS += snap-discard-ns/snap-discard-ns +if HAVE_RST2MAN +dist_man_MANS += snap-discard-ns/snap-discard-ns.5 +CLEANFILES += snap-discard-ns/snap-discard-ns.5 +endif +EXTRA_DIST += snap-discard-ns/snap-discard-ns.rst + +snap_discard_ns_snap_discard_ns_SOURCES = \ + snap-confine/ns-support.c \ + snap-confine/ns-support.h \ + snap-confine/apparmor-support.c \ + snap-confine/apparmor-support.h \ + snap-discard-ns/snap-discard-ns.c +snap_discard_ns_snap_discard_ns_CFLAGS = $(CHECK_CFLAGS) $(AM_CFLAGS) +snap_discard_ns_snap_discard_ns_LDFLAGS = $(AM_LDFLAGS) +snap_discard_ns_snap_discard_ns_LDADD = libsnap-confine-private.a +snap_discard_ns_snap_discard_ns_STATIC = + +if APPARMOR +snap_discard_ns_snap_discard_ns_CFLAGS += $(APPARMOR_CFLAGS) +if STATIC_LIBAPPARMOR +snap_discard_ns_snap_discard_ns_STATIC += $(shell pkg-config --static --libs libapparmor) +else +snap_discard_ns_snap_discard_ns_LDADD += $(APPARMOR_LIBS) +endif # STATIC_LIBAPPARMOR +endif # APPARMOR + +if STATIC_LIBCAP +snap_discard_ns_snap_discard_ns_STATIC += -lcap +else +snap_discard_ns_snap_discard_ns_LDADD += -lcap +endif # STATIC_LIBCAP + +# Use a hacked rule if we're doing static build. This allows us to inject the LIBS += .. rule below. +snap-discard-ns/snap-discard-ns$(EXEEXT): $(snap_discard_ns_snap_discard_ns_OBJECTS) $(snap_discard_ns_snap_discard_ns_DEPENDENCIES) $(EXTRA_snap_discard_ns_snap_discard_ns_DEPENDENCIES) snap-discard-ns/$(am__dirstamp) + @rm -f snap-discard-ns/snap-discard-ns$(EXEEXT) + $(AM_V_CCLD)$(snap_discard_ns_snap_discard_ns_LINK) $(snap_discard_ns_snap_discard_ns_OBJECTS) $(snap_discard_ns_snap_discard_ns_LDADD) $(LIBS) + +snap-discard-ns/snap-discard-ns$(EXEEXT): LIBS += -Wl,-Bstatic $(snap_discard_ns_snap_discard_ns_STATIC) -Wl,-Bdynamic -pthread + +if HAVE_RST2MAN +snap-discard-ns/%.5: snap-discard-ns/%.rst + mkdir -p snap-discard-ns + $(HAVE_RST2MAN) $^ > $@ +endif + +## +## system-shutdown +## + +libexec_PROGRAMS += system-shutdown/system-shutdown + +system_shutdown_system_shutdown_SOURCES = \ + system-shutdown/system-shutdown-utils.c \ + system-shutdown/system-shutdown-utils.h \ + system-shutdown/system-shutdown.c +system_shutdown_system_shutdown_LDADD = libsnap-confine-private.a +system_shutdown_system_shutdown_CFLAGS = $(CHECK_CFLAGS) $(filter-out -fPIE -pie,$(CFLAGS)) -static +system_shutdown_system_shutdown_LDFLAGS = $(filter-out -fPIE -pie,$(LDFLAGS)) -static + +if WITH_UNIT_TESTS +noinst_PROGRAMS += system-shutdown/unit-tests +system_shutdown_unit_tests_SOURCES = \ + libsnap-confine-private/unit-tests-main.c \ + libsnap-confine-private/unit-tests.c \ + system-shutdown/system-shutdown-utils-test.c +system_shutdown_unit_tests_LDADD = libsnap-confine-private.a +system_shutdown_unit_tests_CFLAGS = $(GLIB_CFLAGS) +system_shutdown_unit_tests_LDADD += $(GLIB_LIBS) +endif diff --git a/cmd/autogen.sh b/cmd/autogen.sh new file mode 100755 index 00000000..a4f17f60 --- /dev/null +++ b/cmd/autogen.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# Welcome to the Happy Maintainer's Utility Script +# +# Set BUILD_DIR to the directory where the build will happen, otherwise $PWD +# will be used +set -eux + +BUILD_DIR=${BUILD_DIR:-.} +selfdir=$(dirname "$0") +SRC_DIR=$(readlink -f "$selfdir") + +# We need the VERSION file to configure +if [ ! -e VERSION ]; then + ( cd .. && ./mkversion.sh ) +fi + +# Sanity check, are we in the right directory? +test -f configure.ac + +# Regenerate the build system +rm -f config.status +autoreconf -i -f + +# Configure the build +extra_opts= +# shellcheck disable=SC1091 +. /etc/os-release +case "$ID" in + arch) + extra_opts="--libexecdir=/usr/lib/snapd --with-snap-mount-dir=/var/lib/snapd/snap --disable-apparmor --enable-nvidia-biarch --enable-merged-usr" + ;; + debian) + extra_opts="--libexecdir=/usr/lib/snapd" + ;; + ubuntu) + case "$VERSION_ID" in + 16.04) + extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --enable-static-libseccomp" + ;; + *) + extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap" + ;; + esac + ;; + fedora|centos|rhel) + extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor" + ;; + opensuse) + extra_opts="--libexecdir=/usr/lib/snapd" + ;; + solus) + extra_opts="--enable-nvidia-biarch" + ;; +esac + +echo "Configuring in build directory $BUILD_DIR with: $extra_opts" +mkdir -p "$BUILD_DIR" && cd "$BUILD_DIR" +# shellcheck disable=SC2086 +"${SRC_DIR}/configure" --enable-maintainer-mode --prefix=/usr $extra_opts diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 00000000..53ee92c7 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,205 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package cmd + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "syscall" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/strutil" +) + +// The SNAP_REEXEC environment variable controls whether the command +// will attempt to re-exec itself from inside an ubuntu-core snap +// present on the system. If not present in the environ it's assumed +// to be set to 1 (do re-exec); that is: set it to 0 to disable. +const reExecKey = "SNAP_REEXEC" + +var ( + // newCore is the place to look for the core snap; everything in this + // location will be new enough to re-exec into. + newCore = "/snap/core/current" + + // oldCore is the previous location of the core snap. Only things + // newer than minOldRevno will be ok to re-exec into. + oldCore = "/snap/ubuntu-core/current" + + // selfExe is the path to a symlink pointing to the current executable + selfExe = "/proc/self/exe" + + syscallExec = syscall.Exec + osReadlink = os.Readlink +) + +// distroSupportsReExec returns true if the distribution we are running on can use re-exec. +// +// This is true by default except for a "core/all" snap system where it makes +// no sense and in certain distributions that we don't want to enable re-exec +// yet because of missing validation or other issues. +func distroSupportsReExec() bool { + if !release.OnClassic { + return false + } + if !release.DistroLike("debian", "ubuntu") { + logger.Debugf("re-exec not supported on distro %q yet", release.ReleaseInfo.ID) + return false + } + return true +} + +// coreSupportsReExec returns true if the given core snap should be used as re-exec target. +// +// Ensure we do not use older version of snapd, look for info file and ignore +// version of core that do not yet have it. +func coreSupportsReExec(corePath string) bool { + fullInfo := filepath.Join(corePath, filepath.Join(dirs.CoreLibExecDir, "info")) + if !osutil.FileExists(fullInfo) { + return false + } + content, err := ioutil.ReadFile(fullInfo) + if err != nil { + logger.Noticef("cannot read snapd info file %q: %s", fullInfo, err) + return false + } + ver := regexp.MustCompile("(?m)^VERSION=(.*)$").FindStringSubmatch(string(content)) + if len(ver) != 2 { + logger.Noticef("cannot find snapd version information in %q", content) + return false + } + // > 0 means our Version is bigger than the version of snapd in core + res, err := strutil.VersionCompare(Version, ver[1]) + if err != nil { + logger.Debugf("cannot version compare %q and %q: %s", Version, ver[1], res) + return false + } + if res > 0 { + logger.Debugf("core snap (at %q) is older (%q) than distribution package (%q)", corePath, ver[1], Version) + return false + } + return true +} + +// InternalToolPath returns the path of an internal snapd tool. The tool +// *must* be located inside /usr/lib/snapd/. +// +// The return value is either the path of the tool in the current distribution +// or in the core snap (or the ubuntu-core snap). This handles spiritual +// "re-exec" where we run the tool from the core snap if the environment allows +// us to do so. +func InternalToolPath(tool string) string { + distroTool := filepath.Join(dirs.DistroLibExecDir, tool) + + // find the internal path relative to the running snapd, this + // ensure we don't rely on the state of the system (like + // having a valid "current" symlink). + exe, err := osReadlink("/proc/self/exe") + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v, using tool outside core", err) + return distroTool + } + + // ensure we never use this helper from anything but + if !strings.HasSuffix(exe, "/snapd") && !strings.HasSuffix(exe, ".test") { + log.Panicf("InternalToolPath can only be used from snapd, got: %s", exe) + } + + if !strings.HasPrefix(exe, dirs.SnapMountDir) { + logger.Debugf("exe doesn't have snap mount dir prefix: %q vs %q", exe, dirs.SnapMountDir) + return distroTool + } + + // if we are re-execed, then the tool is at the same location + // as snapd + return filepath.Join(filepath.Dir(exe), tool) +} + +// mustUnsetenv will unset the given environment key or panic if it +// cannot do that +func mustUnsetenv(key string) { + if err := os.Unsetenv(key); err != nil { + log.Panicf("cannot unset %s: %s", key, err) + } +} + +// ExecInCoreSnap makes sure you're executing the binary that ships in +// the core snap. +func ExecInCoreSnap() { + // Which executable are we? + exe, err := os.Readlink(selfExe) + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v", err) + return + } + + // Special case for snapd re-execing from 2.21. In this + // version of snap/snapd we did set SNAP_REEXEC=0 when we + // re-execed. In this case we need to unset the reExecKey to + // ensure that subsequent run of snap/snapd (e.g. when using + // classic confinement) will *not* prevented from re-execing. + if strings.HasPrefix(exe, dirs.SnapMountDir) && !osutil.GetenvBool(reExecKey, true) { + mustUnsetenv(reExecKey) + return + } + + // If we are asked not to re-execute use distribution packages. This is + // "spiritual" re-exec so use the same environment variable to decide. + if !osutil.GetenvBool(reExecKey, true) { + logger.Debugf("re-exec disabled by user") + return + } + + // Did we already re-exec? + if strings.HasPrefix(exe, dirs.SnapMountDir) { + return + } + + // If the distribution doesn't support re-exec or run-from-core then don't do it. + if !distroSupportsReExec() { + return + } + + // Is this executable in the core snap too? + corePath := newCore + full := filepath.Join(newCore, exe) + if !osutil.FileExists(full) { + corePath = oldCore + full = filepath.Join(oldCore, exe) + if !osutil.FileExists(full) { + return + } + } + + // If the core snap doesn't support re-exec or run-from-core then don't do it. + if !coreSupportsReExec(corePath) { + return + } + + logger.Debugf("restarting into %q", full) + panic(syscallExec(full, os.Args, os.Environ())) +} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 00000000..97074bbf --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,307 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package cmd_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/release" +) + +func Test(t *testing.T) { TestingT(t) } + +type cmdSuite struct { + restoreExec func() + restoreLogger func() + execCalled int + lastExecArgv0 string + lastExecArgv []string + lastExecEnvv []string + fakeroot string + newCore string + oldCore string +} + +var _ = Suite(&cmdSuite{}) + +func (s *cmdSuite) SetUpTest(c *C) { + s.restoreExec = cmd.MockSyscallExec(s.syscallExec) + _, s.restoreLogger = logger.MockLogger() + s.execCalled = 0 + s.lastExecArgv0 = "" + s.lastExecArgv = nil + s.lastExecEnvv = nil + s.fakeroot = c.MkDir() + dirs.SetRootDir(s.fakeroot) + s.newCore = filepath.Join(dirs.SnapMountDir, "/core/42") + s.oldCore = filepath.Join(dirs.SnapMountDir, "/ubuntu-core/21") + c.Assert(os.MkdirAll(filepath.Join(s.fakeroot, "proc/self"), 0755), IsNil) +} + +func (s *cmdSuite) TearDownTest(c *C) { + s.restoreExec() + s.restoreLogger() +} + +func (s *cmdSuite) syscallExec(argv0 string, argv []string, envv []string) (err error) { + s.execCalled++ + s.lastExecArgv0 = argv0 + s.lastExecArgv = argv + s.lastExecEnvv = envv + return fmt.Errorf(">exec of %q in tests<", argv0) +} + +func (s *cmdSuite) fakeCoreVersion(c *C, coreDir, version string) { + p := filepath.Join(coreDir, "/usr/lib/snapd") + c.Assert(os.MkdirAll(p, 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(p, "info"), []byte("VERSION="+version), 0644), IsNil) +} + +func (s *cmdSuite) fakeInternalTool(c *C, coreDir, toolName string) string { + s.fakeCoreVersion(c, coreDir, "42") + p := filepath.Join(coreDir, "/usr/lib/snapd", toolName) + c.Assert(ioutil.WriteFile(p, nil, 0755), IsNil) + + return p +} + +func (s *cmdSuite) mockReExecingEnv() func() { + restore := []func(){ + release.MockOnClassic(true), + release.MockReleaseInfo(&release.OS{ID: "ubuntu"}), + cmd.MockCorePaths(s.oldCore, s.newCore), + cmd.MockVersion("2"), + } + + return func() { + for i := len(restore) - 1; i >= 0; i-- { + restore[i]() + } + } +} + +func (s *cmdSuite) mockReExecFor(c *C, coreDir, toolName string) func() { + selfExe := filepath.Join(s.fakeroot, "proc/self/exe") + restore := []func(){ + s.mockReExecingEnv(), + cmd.MockSelfExe(selfExe), + } + s.fakeInternalTool(c, coreDir, toolName) + c.Assert(os.Symlink(filepath.Join("/usr/lib/snapd", toolName), selfExe), IsNil) + + return func() { + for i := len(restore) - 1; i >= 0; i-- { + restore[i]() + } + } +} + +func (s *cmdSuite) TestDistroSupportsReExec(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + // Some distributions don't support re-execution yet. + for _, id := range []string{"fedora", "centos", "rhel", "opensuse", "suse", "poky"} { + restore = release.MockReleaseInfo(&release.OS{ID: id}) + defer restore() + c.Check(cmd.DistroSupportsReExec(), Equals, false, Commentf("ID: %q", id)) + } + + // While others do. + for _, id := range []string{"debian", "ubuntu"} { + restore = release.MockReleaseInfo(&release.OS{ID: id}) + defer restore() + c.Check(cmd.DistroSupportsReExec(), Equals, true, Commentf("ID: %q", id)) + } +} + +func (s *cmdSuite) TestNonClassicDistroNoSupportsReExec(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + // no distro supports re-exec when not on classic :-) + for _, id := range []string{ + "fedora", "centos", "rhel", "opensuse", "suse", "poky", + "debian", "ubuntu", "arch", + } { + restore = release.MockReleaseInfo(&release.OS{ID: id}) + defer restore() + c.Check(cmd.DistroSupportsReExec(), Equals, false, Commentf("ID: %q", id)) + } +} + +func (s *cmdSuite) TestCoreSupportsReExecNoInfo(c *C) { + // there's no snapd/info in a just-created tmpdir :-p + c.Check(cmd.CoreSupportsReExec(c.MkDir()), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExecBadInfo(c *C) { + // can't read snapd/info if it's a directory + p := s.newCore + "/usr/lib/snapd/info" + c.Assert(os.MkdirAll(p, 0755), IsNil) + + c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExecBadInfoContent(c *C) { + // can't understand snapd/info if all it holds are potatoes + p := s.newCore + "/usr/lib/snapd" + c.Assert(os.MkdirAll(p, 0755), IsNil) + c.Assert(ioutil.WriteFile(p+"/info", []byte("potatoes"), 0644), IsNil) + + c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExecBadVersion(c *C) { + // can't understand snapd/info if all its version is gibberish + s.fakeCoreVersion(c, s.newCore, "0:") + + c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExecOldVersion(c *C) { + // can't re-exec if core version is too old + defer cmd.MockVersion("2")() + s.fakeCoreVersion(c, s.newCore, "0") + + c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, false) +} + +func (s *cmdSuite) TestCoreSupportsReExec(c *C) { + defer cmd.MockVersion("2")() + s.fakeCoreVersion(c, s.newCore, "9999") + + c.Check(cmd.CoreSupportsReExec(s.newCore), Equals, true) +} + +func (s *cmdSuite) TestInternalToolPathNoReexec(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.DistroLibExecDir, "snapd"), nil + }) + defer restore() + + c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.DistroLibExecDir, "potato")) +} + +func (s *cmdSuite) TestInternalToolPathWithReexec(c *C) { + s.fakeInternalTool(c, s.newCore, "potato") + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join(s.newCore, "/usr/lib/snapd/snapd"), nil + }) + defer restore() + + c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.SnapMountDir, "core/42/usr/lib/snapd/potato")) +} + +func (s *cmdSuite) TestInternalToolPathFromIncorrectHelper(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return "/usr/bin/potato", nil + }) + defer restore() + + c.Check(func() { cmd.InternalToolPath("potato") }, PanicMatches, "InternalToolPath can only be used from snapd, got: /usr/bin/potato") +} + +func (s *cmdSuite) TestExecInCoreSnap(c *C) { + defer s.mockReExecFor(c, s.newCore, "potato")() + + c.Check(cmd.ExecInCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) + c.Check(s.execCalled, Equals, 1) + c.Check(s.lastExecArgv0, Equals, filepath.Join(s.newCore, "/usr/lib/snapd/potato")) + c.Check(s.lastExecArgv, DeepEquals, os.Args) +} + +func (s *cmdSuite) TestExecInOldCoreSnap(c *C) { + defer s.mockReExecFor(c, s.oldCore, "potato")() + + c.Check(cmd.ExecInCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) + c.Check(s.execCalled, Equals, 1) + c.Check(s.lastExecArgv0, Equals, filepath.Join(s.oldCore, "/usr/lib/snapd/potato")) + c.Check(s.lastExecArgv, DeepEquals, os.Args) +} + +func (s *cmdSuite) TestExecInCoreSnapBailsNoCoreSupport(c *C) { + defer s.mockReExecFor(c, s.newCore, "potato")() + + // no "info" -> no core support: + c.Assert(os.Remove(filepath.Join(s.newCore, "/usr/lib/snapd/info")), IsNil) + + cmd.ExecInCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInCoreSnapMissingExe(c *C) { + defer s.mockReExecFor(c, s.newCore, "potato")() + + // missing exe: + c.Assert(os.Remove(filepath.Join(s.newCore, "/usr/lib/snapd/potato")), IsNil) + + cmd.ExecInCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInCoreSnapBadSelfExe(c *C) { + defer s.mockReExecFor(c, s.newCore, "potato")() + + // missing self/exe: + c.Assert(os.Remove(filepath.Join(s.fakeroot, "proc/self/exe")), IsNil) + + cmd.ExecInCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInCoreSnapBailsNoDistroSupport(c *C) { + defer s.mockReExecFor(c, s.newCore, "potato")() + + // no distro support: + defer release.MockOnClassic(false)() + + cmd.ExecInCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInCoreSnapNoDouble(c *C) { + selfExe := filepath.Join(s.fakeroot, "proc/self/exe") + err := os.Symlink(filepath.Join(s.fakeroot, "/snap/core/42/usr/lib/snapd"), selfExe) + c.Assert(err, IsNil) + cmd.MockSelfExe(selfExe) + + cmd.ExecInCoreSnap() + c.Check(s.execCalled, Equals, 0) +} + +func (s *cmdSuite) TestExecInCoreSnapDisabled(c *C) { + defer s.mockReExecFor(c, s.newCore, "potato")() + + os.Setenv("SNAP_REEXEC", "0") + defer os.Unsetenv("SNAP_REEXEC") + + cmd.ExecInCoreSnap() + c.Check(s.execCalled, Equals, 0) +} diff --git a/cmd/configure.ac b/cmd/configure.ac new file mode 100644 index 00000000..f2e6ce6e --- /dev/null +++ b/cmd/configure.ac @@ -0,0 +1,213 @@ +AC_PREREQ([2.69]) +AC_INIT([snap-confine], m4_esyscmd_s([cat VERSION]), [snapcraft@lists.ubuntu.com]) +AC_CONFIG_SRCDIR([snap-confine/snap-confine.c]) +AC_CONFIG_HEADERS([config.h]) +AC_USE_SYSTEM_EXTENSIONS +AM_INIT_AUTOMAKE([foreign subdir-objects]) +AM_MAINTAINER_MODE([enable]) + +# Checks for programs. +AC_PROG_CC_C99 +AC_PROG_CPP +AC_PROG_INSTALL +AC_PROG_MAKE_SET +AC_PROG_RANLIB + +AC_LANG([C]) +# Checks for libraries. + +# check for large file support +AC_SYS_LARGEFILE + +# Checks for header files. +AC_CHECK_HEADERS([fcntl.h limits.h stdlib.h string.h sys/mount.h unistd.h]) +AC_CHECK_HEADERS([sys/quota.h], [], [AC_MSG_ERROR(sys/quota.h unavailable)]) +AC_CHECK_HEADERS([xfs/xqm.h], [], [AC_MSG_ERROR(xfs/xqm.h unavailable)], +[[#define _GNU_SOURCE +#define _FILE_OFFSET_BITS 64 +#include +]]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_CHECK_HEADER_STDBOOL +AC_TYPE_UID_T +AC_TYPE_MODE_T +AC_TYPE_PID_T +AC_TYPE_SIZE_T + +# Checks for library functions. +AC_FUNC_CHOWN +AC_FUNC_ERROR_AT_LINE +AC_FUNC_FORK +AC_FUNC_STRNLEN +AC_CHECK_FUNCS([mkdir regcomp setenv strdup strerror secure_getenv]) + +AC_ARG_WITH([unit-tests], + AC_HELP_STRING([--without-unit-tests], [do not build unit test programs]), + [case "${withval}" in + yes) with_unit_tests=yes ;; + no) with_unit_tests=no ;; + *) AC_MSG_ERROR([bad value ${withval} for --without-unit-tests]) + esac], [with_unit_tests=yes]) +AM_CONDITIONAL([WITH_UNIT_TESTS], [test "x$with_unit_tests" = "xyes"]) + +# Allow to build without apparmor support by calling: +# ./configure --disable-apparmor +# This makes it possible to run snaps in devmode on almost any host, +# regardless of the kernel version. +AC_ARG_ENABLE([apparmor], + AS_HELP_STRING([--disable-apparmor], [Disable apparmor support]), + [case "${enableval}" in + yes) enable_apparmor=yes ;; + no) enable_apparmor=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --disable-apparmor]) + esac], [enable_apparmor=yes]) +AM_CONDITIONAL([APPARMOR], [test "x$enable_apparmor" = "xyes"]) + +# Allow to build without seccomp support by calling: +# ./configure --disable-seccomp +# This is separate because seccomp support is generally very good and it +# provides useful confinement for unsafe system calls. +AC_ARG_ENABLE([seccomp], + AS_HELP_STRING([--disable-seccomp], [Disable seccomp support]), + [case "${enableval}" in + yes) enable_seccomp=yes ;; + no) enable_seccomp=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --disable-seccomp]) + esac], [enable_seccomp=yes]) +AM_CONDITIONAL([SECCOMP], [test "x$enable_seccomp" = "xyes"]) + +# Enable older tests only when confinement is enabled and we're building for PC +# The tests are of smaller value as we port more and more tests to spread. +AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && test "x$enable_seccomp" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))]) + +# Check for glib that we use for unit testing +AS_IF([test "x$with_unit_tests" = "xyes"], [ + PKG_CHECK_MODULES([GLIB], [glib-2.0]) +]) + +# Check if seccomp userspace library is available +AS_IF([test "x$enable_seccomp" = "xyes"], [ + PKG_CHECK_MODULES([SECCOMP], [libseccomp], [ + AC_DEFINE([HAVE_SECCOMP], [1], [Build with seccomp support])]) +]) + +# Check if apparmor userspace library is available. +AS_IF([test "x$enable_apparmor" = "xyes"], [ + PKG_CHECK_MODULES([APPARMOR], [libapparmor], [ + AC_DEFINE([HAVE_APPARMOR], [1], [Build with apparmor support])]) +], [ + AC_MSG_WARN([ + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + X X + X Apparmor is disabled, all snaps will run in devmode X + X X + XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]) +]) + +# Check if udev and libudev are available. +# Those are now used unconditionally even if apparmor is disabled. +PKG_CHECK_MODULES([LIBUDEV], [libudev]) +PKG_CHECK_MODULES([UDEV], [udev]) + +# Check if libcap is available. +# PKG_CHECK_MODULES([LIBCAP], [libcap]) + +# Enable special support for hosts with proprietary nvidia drivers on Ubuntu. +AC_ARG_ENABLE([nvidia-multiarch], + AS_HELP_STRING([--enable-nvidia-multiarch], [Support for proprietary nvidia drivers (Ubuntu/Debian)]), + [case "${enableval}" in + yes) enable_nvidia_multiarch=yes ;; + no) enable_nvidia_multiarch=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-multiarch]) + esac], [enable_nvidia_multiarch=no]) +AM_CONDITIONAL([NVIDIA_MULTIARCH], [test "x$enable_nvidia_multiarch" = "xyes"]) + +AS_IF([test "x$enable_nvidia_multiarch" = "xyes"], [ + AC_DEFINE([NVIDIA_MULTIARCH], [1], + [Support for proprietary nvidia drivers (Ubuntu/Debian)])]) + +# Enable special support for hosts with proprietary nvidia drivers on Arch. +AC_ARG_ENABLE([nvidia-biarch], + AS_HELP_STRING([--enable-nvidia-biarch], [Support for proprietary nvidia drivers (bi-arch distributions)]), + [case "${enableval}" in + yes) enable_nvidia_biarch=yes ;; + no) enable_nvidia_biarch=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-biarch]) + esac], [enable_nvidia_biarch=no]) +AM_CONDITIONAL([NVIDIA_BIARCH], [test "x$enable_nvidia_biarch" = "xyes"]) + +AS_IF([test "x$enable_nvidia_biarch" = "xyes"], [ + AC_DEFINE([NVIDIA_BIARCH], [1], + [Support for proprietary nvidia drivers (bi-arch distributions)])]) + +AC_ARG_ENABLE([merged-usr], + AS_HELP_STRING([--enable-merged-usr], [Enable support for merged /usr directory]), + [case "${enableval}" in + yes) enable_merged_usr=yes ;; + no) enable_merged_usr=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-merged-usr]) + esac], [enable_merged_usr=no]) +AM_CONDITIONAL([MERGED_USR], [test "x$enable_merged_usr" = "xyes"]) + +AS_IF([test "x$enable_merged_usr" = "xyes"], [ + AC_DEFINE([MERGED_USR], [1], + [Support for merged /usr directory])]) + +SNAP_MOUNT_DIR="/snap" +AC_ARG_WITH([snap-mount-dir], + AS_HELP_STRING([--with-snap-mount-dir=DIR], [Use an alternate snap mount directory]), + [SNAP_MOUNT_DIR="$withval"]) +AC_SUBST(SNAP_MOUNT_DIR) +AC_DEFINE_UNQUOTED([SNAP_MOUNT_DIR], "${SNAP_MOUNT_DIR}", [Location of the snap mount points]) + +AC_ARG_ENABLE([caps-over-setuid], + AS_HELP_STRING([--enable-caps-over-setuid], [Use capabilities rather than setuid bit]), + [case "${enableval}" in + yes) enable_caps_over_setuid=yes ;; + no) enable_caps_over_setuid=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-caps-over-setuid]) + esac], [enable_caps_over_setuid=no]) +AM_CONDITIONAL([CAPS_OVER_SETUID], [test "x$enable_caps_over_setuid" = "xyes"]) + +AS_IF([test "x$enable_caps_over_setuid" = "xyes"], [ + AC_DEFINE([CAPS_OVER_SETUID], [1], + [Use capabilities rather than setuid bit])]) + +AC_PATH_PROGS([HAVE_RST2MAN],[rst2man rst2man.py]) +AS_IF([test "x$HAVE_RST2MAN" = "x"], [AC_MSG_WARN(["cannot find the rst2man tool, install python-docutils or similar"])]) +AM_CONDITIONAL([HAVE_RST2MAN], [test "x${HAVE_RST2MAN}" != "x"]) + +AC_PATH_PROG([HAVE_VALGRIND],[valgrind]) +AM_CONDITIONAL([HAVE_VALGRIND], [test "x${HAVE_VALGRIND}" != "x"]) +AS_IF([test "x$HAVE_VALGRIND" = "x"], [AC_MSG_WARN(["cannot find the valgrind tool, will not run unit tests through valgrind"])]) + +AC_ARG_ENABLE([static-libcap], + AS_HELP_STRING([--enable-static-libcap], [Link libcap statically]), + [case "${enableval}" in + yes) enable_static_libcap=yes ;; + no) enable_static_libcap=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libcap]) + esac], [enable_static_libcap=no]) +AM_CONDITIONAL([STATIC_LIBCAP], [test "x$enable_static_libcap" = "xyes"]) + +AC_ARG_ENABLE([static-libapparmor], + AS_HELP_STRING([--enable-static-libapparmor], [Link libapparmor statically]), + [case "${enableval}" in + yes) enable_static_libapparmor=yes ;; + no) enable_static_libapparmor=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libapparmor]) + esac], [enable_static_libapparmor=no]) +AM_CONDITIONAL([STATIC_LIBAPPARMOR], [test "x$enable_static_libapparmor" = "xyes"]) + +AC_ARG_ENABLE([static-libseccomp], + AS_HELP_STRING([--enable-static-libseccomp], [Link libseccomp statically]), + [case "${enableval}" in + yes) enable_static_libseccomp=yes ;; + no) enable_static_libseccomp=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libseccomp]) + esac], [enable_static_libseccomp=no]) +AM_CONDITIONAL([STATIC_LIBSECCOMP], [test "x$enable_static_libseccomp" = "xyes"]) + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/cmd/decode-mount-opts/decode-mount-opts.c b/cmd/decode-mount-opts/decode-mount-opts.c new file mode 100644 index 00000000..383aa03b --- /dev/null +++ b/cmd/decode-mount-opts/decode-mount-opts.c @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include +#include + +#include "../libsnap-confine-private/mount-opt.h" + +int main(int argc, char *argv[]) +{ + if (argc != 2) { + printf("usage: decode-mount-opts OPT\n"); + return 0; + } + char *end; + unsigned long mountflags = strtoul(argv[1], &end, 0); + if (*end != '\0') { + fprintf(stderr, "cannot parse given argument as a number\n"); + return 1; + } + char buf[1000] = {0}; + printf("%#lx is %s\n", mountflags, sc_mount_opt2str(buf, sizeof buf, mountflags)); + return 0; +} diff --git a/cmd/export_test.go b/cmd/export_test.go new file mode 100644 index 00000000..77634923 --- /dev/null +++ b/cmd/export_test.go @@ -0,0 +1,60 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package cmd + +var ( + DistroSupportsReExec = distroSupportsReExec + CoreSupportsReExec = coreSupportsReExec +) + +func MockCorePaths(newOldCore, newNewCore string) func() { + oldOldCore := oldCore + oldNewCore := newCore + newCore = newNewCore + oldCore = newOldCore + return func() { + newCore = oldNewCore + oldCore = oldOldCore + } +} + +func MockSelfExe(newSelfExe string) func() { + oldSelfExe := selfExe + selfExe = newSelfExe + return func() { + selfExe = oldSelfExe + } +} + +func MockSyscallExec(f func(argv0 string, argv []string, envv []string) (err error)) func() { + oldSyscallExec := syscallExec + syscallExec = f + return func() { + syscallExec = oldSyscallExec + } +} + +func MockOsReadlink(f func(string) (string, error)) func() { + realOsReadlink := osReadlink + osReadlink = f + return func() { + osReadlink = realOsReadlink + } +} diff --git a/cmd/libsnap-confine-private/cgroup-freezer-support.c b/cmd/libsnap-confine-private/cgroup-freezer-support.c new file mode 100644 index 00000000..39fac02a --- /dev/null +++ b/cmd/libsnap-confine-private/cgroup-freezer-support.c @@ -0,0 +1,67 @@ +// For AT_EMPTY_PATH and O_PATH +#define _GNU_SOURCE + +#include "cgroup-freezer-support.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "cleanup-funcs.h" +#include "string-utils.h" +#include "utils.h" + +static const char *freezer_cgroup_dir = "/sys/fs/cgroup/freezer"; + +void sc_cgroup_freezer_join(const char *snap_name, pid_t pid) +{ + // Format the name of the cgroup hierarchy. + char buf[PATH_MAX] = { 0 }; + sc_must_snprintf(buf, sizeof buf, "snap.%s", snap_name); + + // Open the freezer cgroup directory. + int cgroup_fd SC_CLEANUP(sc_cleanup_close) = -1; + cgroup_fd = open(freezer_cgroup_dir, + O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (cgroup_fd < 0) { + die("cannot open freezer cgroup (%s)", freezer_cgroup_dir); + } + // Create the freezer hierarchy for the given snap. + if (mkdirat(cgroup_fd, buf, 0755) < 0 && errno != EEXIST) { + die("cannot create freezer cgroup hierarchy for snap %s", + snap_name); + } + // Open the hierarchy directory for the given snap. + int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1; + hierarchy_fd = openat(cgroup_fd, buf, + O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (hierarchy_fd < 0) { + die("cannot open freezer cgroup hierarchy for snap %s", + snap_name); + } + // Since we may be running from a setuid but not setgid executable, ensure + // that the group and owner of the hierarchy directory is root.root. + if (fchownat(hierarchy_fd, "", 0, 0, AT_EMPTY_PATH) < 0) { + die("cannot change owner of freezer cgroup hierarchy for snap %s to root.root", snap_name); + } + // Open the tasks file. + int tasks_fd SC_CLEANUP(sc_cleanup_close) = -1; + tasks_fd = openat(hierarchy_fd, "tasks", + O_WRONLY | O_NOFOLLOW | O_CLOEXEC); + if (tasks_fd < 0) { + die("cannot open tasks file for freezer cgroup hierarchy for snap %s", snap_name); + } + // Write the process (task) number to the tasks file. Linux task IDs are + // limited to 2^29 so a long int is enough to represent it. + // See include/linux/threads.h in the kernel source tree for details. + int n = sc_must_snprintf(buf, sizeof buf, "%ld", (long)pid); + if (write(tasks_fd, buf, n) < n) { + die("cannot move process %ld to freezer cgroup hierarchy for snap %s", (long)pid, snap_name); + } + debug("moved process %ld to freezer cgroup hierarchy for snap %s", + (long)pid, snap_name); +} diff --git a/cmd/libsnap-confine-private/cgroup-freezer-support.h b/cmd/libsnap-confine-private/cgroup-freezer-support.h new file mode 100644 index 00000000..06f9be7a --- /dev/null +++ b/cmd/libsnap-confine-private/cgroup-freezer-support.h @@ -0,0 +1,26 @@ +#ifndef SC_CGROUP_FREEZER_SUPPORT_H +#define SC_CGROUP_FREEZER_SUPPORT_H + +#include +#include "error.h" + +/** + * Join the freezer cgroup for the given snap. + * + * This function adds the specified task to the freezer cgroup specific to the + * given snap. The name of the cgroup is "snap.$snap_name". + * + * Interestingly we don't need to actually freeze the processes. The group + * allows us to track processes belonging to a given snap. This makes the + * measurement "are any processes of this snap still alive" very simple. + * + * The "tasks" file belonging to the cgroup contains the set of all the + * processes that originate from the given snap. Examining that file one can + * reliably determine if the set is empty or not. + * + * For more details please review: + * https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt +**/ +void sc_cgroup_freezer_join(const char *snap_name, pid_t pid); + +#endif diff --git a/cmd/libsnap-confine-private/classic-test.c b/cmd/libsnap-confine-private/classic-test.c new file mode 100644 index 00000000..bc69c827 --- /dev/null +++ b/cmd/libsnap-confine-private/classic-test.c @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "classic.h" +#include "classic.c" + +#include + +const char *os_release_classic = "" + "NAME=\"Ubuntu\"\n" + "VERSION=\"17.04 (Zesty Zapus)\"\n" "ID=ubuntu\n" "ID_LIKE=debian\n"; + +static void test_is_on_classic(void) +{ + g_file_set_contents("os-release.classic", os_release_classic, + strlen(os_release_classic), NULL); + os_release = "os-release.classic"; + g_assert_true(is_running_on_classic_distribution()); + unlink("os-release.classic"); +} + +const char *os_release_core = "" + "NAME=\"Ubuntu Core\"\n" "VERSION=\"16\"\n" "ID=ubuntu-core\n"; + +static void test_is_on_core(void) +{ + g_file_set_contents("os-release.core", os_release_core, + strlen(os_release_core), NULL); + os_release = "os-release.core"; + g_assert_false(is_running_on_classic_distribution()); + unlink("os-release.core"); +} + +const char *os_release_classic_with_long_line = "" + "NAME=\"Ubuntu\"\n" + "VERSION=\"17.04 (Zesty Zapus)\"\n" + "ID=ubuntu\n" + "ID_LIKE=debian\n" + "LONG=line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line.line."; + +static void test_is_on_classic_with_long_line(void) +{ + g_file_set_contents("os-release.classic-with-long-line", + os_release_classic, strlen(os_release_classic), + NULL); + os_release = "os-release.classic-with-long-line"; + g_assert_true(is_running_on_classic_distribution()); + unlink("os-release.classic-with-long-line"); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/classic/on-classic", test_is_on_classic); + g_test_add_func("/classic/on-classic-with-long-line", + test_is_on_classic_with_long_line); + g_test_add_func("/classic/on-core", test_is_on_core); +} diff --git a/cmd/libsnap-confine-private/classic.c b/cmd/libsnap-confine-private/classic.c new file mode 100644 index 00000000..5d63f380 --- /dev/null +++ b/cmd/libsnap-confine-private/classic.c @@ -0,0 +1,25 @@ +#include "config.h" +#include "classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" + +#include +#include +#include + +char *os_release = "/etc/os-release"; + +bool is_running_on_classic_distribution() +{ + FILE *f SC_CLEANUP(sc_cleanup_file) = fopen(os_release, "r"); + if (f == NULL) { + return true; + } + + char buf[255] = { 0 }; + while (fgets(buf, sizeof buf, f) != NULL) { + if (strcmp(buf, "ID=ubuntu-core\n") == 0) { + return false; + } + } + return true; +} diff --git a/cmd/libsnap-confine-private/classic.h b/cmd/libsnap-confine-private/classic.h new file mode 100644 index 00000000..f877140e --- /dev/null +++ b/cmd/libsnap-confine-private/classic.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#ifndef SNAP_CONFINE_CLASSIC_H +#define SNAP_CONFINE_CLASSIC_H + +#include + +// Location of the host filesystem directory in the core snap. +#define SC_HOSTFS_DIR "/var/lib/snapd/hostfs" + +bool is_running_on_classic_distribution(void); + +#endif diff --git a/cmd/libsnap-confine-private/cleanup-funcs-test.c b/cmd/libsnap-confine-private/cleanup-funcs-test.c new file mode 100644 index 00000000..3565892f --- /dev/null +++ b/cmd/libsnap-confine-private/cleanup-funcs-test.c @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "cleanup-funcs.h" +#include "cleanup-funcs.c" + +#include + +static int called = 0; + +static void cleanup_fn(int *ptr) +{ + called = 1; +} + +// Test that cleanup functions are applied as expected +static void test_cleanup_sanity(void) +{ + { + int test SC_CLEANUP(cleanup_fn); + test = 0; + test++; + } + g_assert_cmpint(called, ==, 1); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/cleanup/sanity", test_cleanup_sanity); +} diff --git a/cmd/libsnap-confine-private/cleanup-funcs.c b/cmd/libsnap-confine-private/cleanup-funcs.c new file mode 100644 index 00000000..44005e70 --- /dev/null +++ b/cmd/libsnap-confine-private/cleanup-funcs.c @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "cleanup-funcs.h" + +#include +#include + +void sc_cleanup_string(char **ptr) +{ + if (ptr != NULL) { + free(*ptr); + } +} + +void sc_cleanup_file(FILE ** ptr) +{ + if (ptr != NULL && *ptr != NULL) { + fclose(*ptr); + } +} + +void sc_cleanup_endmntent(FILE ** ptr) +{ + if (ptr != NULL && *ptr != NULL) { + endmntent(*ptr); + } +} + +void sc_cleanup_closedir(DIR ** ptr) +{ + if (ptr != NULL && *ptr != NULL) { + closedir(*ptr); + } +} + +void sc_cleanup_close(int *ptr) +{ + if (ptr != NULL && *ptr != -1) { + close(*ptr); + } +} diff --git a/cmd/libsnap-confine-private/cleanup-funcs.h b/cmd/libsnap-confine-private/cleanup-funcs.h new file mode 100644 index 00000000..5bfe214b --- /dev/null +++ b/cmd/libsnap-confine-private/cleanup-funcs.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_CLEANUP_FUNCS_H +#define SNAP_CONFINE_CLEANUP_FUNCS_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif // HAVE_CONFIG_H + +#include +#include +#include +#include + +// SC_CLEANUP will run the given cleanup function when the variable next +// to it goes out of scope. +#define SC_CLEANUP(n) __attribute__((cleanup(n))) + +/** + * Free a dynamically allocated string. + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_string))). + **/ +void sc_cleanup_string(char **ptr); + +/** + * Close an open file. + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_file))). + **/ +void sc_cleanup_file(FILE ** ptr); + +/** + * Close an open file with endmntent(3) + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_endmntent))). + **/ +void sc_cleanup_endmntent(FILE ** ptr); + +/** + * Close an open directory with closedir(3) + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_closedir))). + **/ +void sc_cleanup_closedir(DIR ** ptr); + +/** + * Close an open file descriptor with close(2) + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_close))). + **/ +void sc_cleanup_close(int *ptr); + +#endif diff --git a/cmd/libsnap-confine-private/error-test.c b/cmd/libsnap-confine-private/error-test.c new file mode 100644 index 00000000..87cf705b --- /dev/null +++ b/cmd/libsnap-confine-private/error-test.c @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "error.h" +#include "error.c" + +#include +#include + +static void test_sc_error_init(void) +{ + struct sc_error *err; + // Create an error + err = sc_error_init("domain", 42, "printer is on %s", "fire"); + g_assert_nonnull(err); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + + // Inspect the exposed attributes + g_assert_cmpstr(sc_error_domain(err), ==, "domain"); + g_assert_cmpint(sc_error_code(err), ==, 42); + g_assert_cmpstr(sc_error_msg(err), ==, "printer is on fire"); +} + +static void test_sc_error_init_from_errno(void) +{ + struct sc_error *err; + // Create an error + err = sc_error_init_from_errno(ENOENT, "printer is on %s", "fire"); + g_assert_nonnull(err); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + + // Inspect the exposed attributes + g_assert_cmpstr(sc_error_domain(err), ==, SC_ERRNO_DOMAIN); + g_assert_cmpint(sc_error_code(err), ==, ENOENT); + g_assert_cmpstr(sc_error_msg(err), ==, "printer is on fire"); +} + +static void test_sc_error_cleanup(void) +{ + // Check that sc_error_cleanup() is safe to use. + + // Cleanup is safe on NULL errors. + struct sc_error *err = NULL; + sc_cleanup_error(&err); + + // Cleanup is safe on non-NULL errors. + err = sc_error_init("domain", 123, "msg"); + g_assert_nonnull(err); + sc_cleanup_error(&err); + g_assert_null(err); +} + +static void test_sc_error_domain__NULL(void) +{ + // Check that sc_error_domain() dies if called with NULL error. + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error *err = NULL; + const char *domain = sc_error_domain(err); + (void)(domain); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot obtain error domain from NULL error\n"); +} + +static void test_sc_error_code__NULL(void) +{ + // Check that sc_error_code() dies if called with NULL error. + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error *err = NULL; + int code = sc_error_code(err); + (void)(code); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot obtain error code from NULL error\n"); +} + +static void test_sc_error_msg__NULL(void) +{ + // Check that sc_error_msg() dies if called with NULL error. + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error *err = NULL; + const char *msg = sc_error_msg(err); + (void)(msg); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot obtain error message from NULL error\n"); +} + +static void test_sc_die_on_error__NULL(void) +{ + // Check that sc_die_on_error() does nothing if called with NULL error. + if (g_test_subprocess()) { + sc_die_on_error(NULL); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_passed(); +} + +static void test_sc_die_on_error__regular(void) +{ + // Check that sc_die_on_error() dies if called with an error. + if (g_test_subprocess()) { + struct sc_error *err = + sc_error_init("domain", 42, "just testing"); + sc_die_on_error(err); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("just testing\n"); +} + +static void test_sc_die_on_error__errno(void) +{ + // Check that sc_die_on_error() dies if called with an errno-based error. + if (g_test_subprocess()) { + struct sc_error *err = + sc_error_init_from_errno(ENOENT, "just testing"); + sc_die_on_error(err); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("just testing: No such file or directory\n"); +} + +static void test_sc_error_forward__nothing(void) +{ + // Check that forwarding NULL does exactly that. + struct sc_error *recipient = (void *)0xDEADBEEF; + struct sc_error *err = NULL; + sc_error_forward(&recipient, err); + g_assert_null(recipient); +} + +static void test_sc_error_forward__something_somewhere(void) +{ + // Check that forwarding a real error works OK. + struct sc_error *recipient = NULL; + struct sc_error *err = sc_error_init("domain", 42, "just testing"); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + g_assert_nonnull(err); + sc_error_forward(&recipient, err); + g_assert_nonnull(recipient); +} + +static void test_sc_error_forward__something_nowhere(void) +{ + // Check that forwarding a real error nowhere calls die() + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error **err_ptr = NULL; + struct sc_error *err = + sc_error_init("domain", 42, "just testing"); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + g_assert_nonnull(err); + sc_error_forward(err_ptr, err); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("just testing\n"); +} + +static void test_sc_error_match__typical(void) +{ + // NULL error doesn't match anything. + g_assert_false(sc_error_match(NULL, "domain", 42)); + + // Non-NULL error matches if domain and code both match. + struct sc_error *err = sc_error_init("domain", 42, "just testing"); + g_test_queue_destroy((GDestroyNotify) sc_error_free, err); + g_assert_true(sc_error_match(err, "domain", 42)); + g_assert_false(sc_error_match(err, "domain", 1)); + g_assert_false(sc_error_match(err, "other-domain", 42)); + g_assert_false(sc_error_match(err, "other-domain", 1)); +} + +static void test_sc_error_match__NULL_domain(void) +{ + // Using a NULL domain is a fatal bug. + if (g_test_subprocess()) { + // NOTE: the code below fools gcc 5.4 but your mileage may vary. + struct sc_error *err = NULL; + const char *domain = NULL; + g_assert_false(sc_error_match(err, domain, 42)); + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot match error to a NULL domain\n"); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/error/sc_error_init", test_sc_error_init); + g_test_add_func("/error/sc_error_init_from_errno", + test_sc_error_init_from_errno); + g_test_add_func("/error/sc_error_cleanup", test_sc_error_cleanup); + g_test_add_func("/error/sc_error_domain/NULL", + test_sc_error_domain__NULL); + g_test_add_func("/error/sc_error_code/NULL", test_sc_error_code__NULL); + g_test_add_func("/error/sc_error_msg/NULL", test_sc_error_msg__NULL); + g_test_add_func("/error/sc_die_on_error/NULL", + test_sc_die_on_error__NULL); + g_test_add_func("/error/sc_die_on_error/regular", + test_sc_die_on_error__regular); + g_test_add_func("/error/sc_die_on_error/errno", + test_sc_die_on_error__errno); + g_test_add_func("/error/sc_error_formward/nothing", + test_sc_error_forward__nothing); + g_test_add_func("/error/sc_error_formward/something_somewhere", + test_sc_error_forward__something_somewhere); + g_test_add_func("/error/sc_error_formward/something_nowhere", + test_sc_error_forward__something_nowhere); + g_test_add_func("/error/sc_error_match/typical", + test_sc_error_match__typical); + g_test_add_func("/error/sc_error_match/NULL_domain", + test_sc_error_match__NULL_domain); +} diff --git a/cmd/libsnap-confine-private/error.c b/cmd/libsnap-confine-private/error.c new file mode 100644 index 00000000..21faaf76 --- /dev/null +++ b/cmd/libsnap-confine-private/error.c @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "error.h" + +// To get vasprintf +#define _GNU_SOURCE + +#include "utils.h" + +#include +#include +#include +#include + +struct sc_error { + // Error domain defines a scope for particular error codes. + const char *domain; + // Code differentiates particular errors for the programmer. + // The code may be zero if the particular meaning is not relevant. + int code; + // Message carries a formatted description of the problem. + char *msg; +}; + +static struct sc_error *sc_error_initv(const char *domain, int code, + const char *msgfmt, va_list ap) +{ + struct sc_error *err = calloc(1, sizeof *err); + if (err == NULL) { + die("cannot allocate memory for error object"); + } + err->domain = domain; + err->code = code; + if (vasprintf(&err->msg, msgfmt, ap) == -1) { + die("cannot format error message"); + } + return err; +} + +struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, + ...) +{ + va_list ap; + va_start(ap, msgfmt); + struct sc_error *err = sc_error_initv(domain, code, msgfmt, ap); + va_end(ap); + return err; +} + +struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, + ...) +{ + va_list ap; + va_start(ap, msgfmt); + struct sc_error *err = + sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap); + va_end(ap); + return err; +} + +const char *sc_error_domain(struct sc_error *err) +{ + if (err == NULL) { + die("cannot obtain error domain from NULL error"); + } + return err->domain; +} + +int sc_error_code(struct sc_error *err) +{ + if (err == NULL) { + die("cannot obtain error code from NULL error"); + } + return err->code; +} + +const char *sc_error_msg(struct sc_error *err) +{ + if (err == NULL) { + die("cannot obtain error message from NULL error"); + } + return err->msg; +} + +void sc_error_free(struct sc_error *err) +{ + if (err != NULL) { + free(err->msg); + err->msg = NULL; + free(err); + } +} + +void sc_cleanup_error(struct sc_error **ptr) +{ + sc_error_free(*ptr); + *ptr = NULL; +} + +void sc_die_on_error(struct sc_error *error) +{ + if (error != NULL) { + if (strcmp(sc_error_domain(error), SC_ERRNO_DOMAIN) == 0) { + // Set errno just before the call to die() as it is used internally + errno = sc_error_code(error); + die("%s", sc_error_msg(error)); + } else { + errno = 0; + die("%s", sc_error_msg(error)); + } + } +} + +void sc_error_forward(struct sc_error **recipient, struct sc_error *error) +{ + if (recipient != NULL) { + *recipient = error; + } else { + sc_die_on_error(error); + } +} + +bool sc_error_match(struct sc_error *error, const char *domain, int code) +{ + if (domain == NULL) { + die("cannot match error to a NULL domain"); + } + if (error == NULL) { + return false; + } + return strcmp(sc_error_domain(error), domain) == 0 + && sc_error_code(error) == code; +} diff --git a/cmd/libsnap-confine-private/error.h b/cmd/libsnap-confine-private/error.h new file mode 100644 index 00000000..71db201d --- /dev/null +++ b/cmd/libsnap-confine-private/error.h @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_ERROR_H +#define SNAP_CONFINE_ERROR_H + +#include + +#define SC_GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) + +/** + * The attribute returns_nonnull is only supported by GCC versions >= 4.9.0. + * Enable building of snap-confine on platforms that are stuck with older + * GCC versions. + **/ +#if SC_GCC_VERSION >= 40900 +#define SC_APPEND_RETURNS_NONNULL , returns_nonnull +#else +#define SC_APPEND_RETURNS_NONNULL +#endif + +/** + * This module defines APIs for simple error management. + * + * Errors are allocated objects that can be returned and passed around from + * functions. Errors carry a formatted message and optionally a scoped error + * code. The code is coped with a string "domain" that simply acts as a + * namespace for various interacting modules. + **/ + +/** + * Opaque error structure. + **/ +struct sc_error; + +/** + * Error domain for errors related to system errno. + **/ +#define SC_ERRNO_DOMAIN "errno" + +/** + * Initialize a new error object. + * + * The domain is a cookie-like string that allows the caller to distinguish + * between "namespaces" of error codes. It should be a static string that is + * provided by the caller. Both the domain and the error code can be retrieved + * later. + * + * This function calls die() in case of memory allocation failure. + **/ +__attribute__ ((warn_unused_result, + format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL)) +struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, + ...); + +/** + * Initialize an errno-based error. + * + * The error carries a copy of errno and a custom error message as designed by + * the caller. See sc_error_init() for a more complete description. + * + * This function calls die() in case of memory allocation failure. + **/ +__attribute__ ((warn_unused_result, + format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL)) +struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, + ...); + +/** + * Get the error domain out of an error object. + * + * The error domain acts as a namespace for error codes. + * No change of ownership takes place. + **/ +__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL)) +const char *sc_error_domain(struct sc_error *err); + +/** + * Get the error code out of an error object. + * + * The error code is scoped by the error domain. + * + * An error code of zero is special-cased to indicate that no particular error + * code is reserved for this error and it's not something that the programmer + * can rely on programmatically. This can be used to return an error message + * without having to allocate a distinct code for each one. + **/ +__attribute__ ((warn_unused_result)) +int sc_error_code(struct sc_error *err); + +/** + * Get the error message out of an error object. + * + * The error message is bound to the life-cycle of the error object. + * No change of ownership takes place. + **/ +__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL)) +const char *sc_error_msg(struct sc_error *err); + +/** + * Free an error object. + * + * The error object can be NULL. + **/ +void sc_error_free(struct sc_error *error); + +/** + * Cleanup an error with sc_error_free() + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_error))). + **/ +__attribute__ ((nonnull)) +void sc_cleanup_error(struct sc_error **ptr); + +/** + * + * Die if there's an error. + * + * This function is a correct way to die() if the passed error is not NULL. + * + * The error message is derived from the data in the error, using the special + * errno domain to provide additional information if that is available. + **/ +void sc_die_on_error(struct sc_error *error); + +/** + * Forward an error to the caller. + * + * This tries to forward an error to the caller. If this is impossible because + * the caller did not provide a location for the error to be stored then the + * sc_die_on_error() is called as a safety measure. + * + * Change of ownership takes place and the error is now stored in the recipient. + **/ +// NOTE: There's no nonnull(1) attribute as the recipient *can* be NULL. With +// the attribute in place GCC optimizes some things out and tests fail. +void sc_error_forward(struct sc_error **recipient, struct sc_error *error); + +/** + * Check if a given error matches the specified domain and code. + * + * It is okay to match a NULL error, the function simply returns false in that + * case. The domain cannot be NULL though. + **/ +__attribute__ ((warn_unused_result)) +bool sc_error_match(struct sc_error *error, const char *domain, int code); + +#endif diff --git a/cmd/libsnap-confine-private/fault-injection-test.c b/cmd/libsnap-confine-private/fault-injection-test.c new file mode 100644 index 00000000..1d62f593 --- /dev/null +++ b/cmd/libsnap-confine-private/fault-injection-test.c @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "fault-injection.h" +#include "fault-injection.c" + +#include +#include + +static bool broken(struct sc_fault_state *state, void *ptr) +{ + return true; +} + +static bool broken_alter_msg(struct sc_fault_state *state, void *ptr) +{ + char **s = ptr; + *s = "broken"; + return true; +} + +static void test_fault_injection(void) +{ + g_assert_false(sc_faulty("foo", NULL)); + + sc_break("foo", broken); + g_assert_true(sc_faulty("foo", NULL)); + + sc_reset_faults(); + g_assert_false(sc_faulty("foo", NULL)); + + const char *msg = NULL; + if (!sc_faulty("foo", &msg)) { + msg = "working"; + } + g_assert_cmpstr(msg, ==, "working"); + + sc_break("foo", broken_alter_msg); + if (!sc_faulty("foo", &msg)) { + msg = "working"; + } + g_assert_cmpstr(msg, ==, "broken"); + sc_reset_faults(); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/fault-injection", test_fault_injection); +} diff --git a/cmd/libsnap-confine-private/fault-injection.c b/cmd/libsnap-confine-private/fault-injection.c new file mode 100644 index 00000000..c8486843 --- /dev/null +++ b/cmd/libsnap-confine-private/fault-injection.c @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "fault-injection.h" + +#ifdef _ENABLE_FAULT_INJECTION + +#include +#include + +struct sc_fault { + const char *name; + struct sc_fault *next; + sc_fault_fn fn; + struct sc_fault_state state; +}; + +static struct sc_fault *sc_faults = NULL; + +bool sc_faulty(const char *name, void *ptr) +{ + for (struct sc_fault * fault = sc_faults; fault != NULL; + fault = fault->next) { + if (strcmp(name, fault->name) == 0) { + bool is_faulty = fault->fn(&fault->state, ptr); + fault->state.ncalls++; + return is_faulty; + } + } + return false; +} + +void sc_break(const char *name, sc_fault_fn fn) +{ + struct sc_fault *fault = calloc(1, sizeof *fault); + if (fault == NULL) { + abort(); + } + fault->name = name; + fault->next = sc_faults; + fault->fn = fn; + fault->state.ncalls = 0; + sc_faults = fault; +} + +void sc_reset_faults(void) +{ + struct sc_fault *next_fault; + for (struct sc_fault * fault = sc_faults; fault != NULL; + fault = next_fault) { + next_fault = fault->next; + free(fault); + } + sc_faults = NULL; +} + +#else // ifndef _ENABLE_FAULT_INJECTION + +bool sc_faulty(const char *name, void *ptr) +{ + return false; +} + +#endif // ifndef _ENABLE_FAULT_INJECTION diff --git a/cmd/libsnap-confine-private/fault-injection.h b/cmd/libsnap-confine-private/fault-injection.h new file mode 100644 index 00000000..cd7c573a --- /dev/null +++ b/cmd/libsnap-confine-private/fault-injection.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_FAULT_INJECTION_H +#define SNAP_CONFINE_FAULT_INJECTION_H + +#include + +/** + * Check for an injected fault. + * + * The name of the fault must match what was passed to sc_break(). The second + * argument can be modified by the fault callback function. The return value + * indicates if a fault was injected. It is assumed that once a fault was + * injected the passed pointer was used to modify the state in useful way. + * + * When the pre-processor macro _ENABLE_FAULT_INJECTION is not defined this + * function always returns false and does nothing at all. + **/ +bool sc_faulty(const char *name, void *ptr); + +#ifdef _ENABLE_FAULT_INJECTION + +struct sc_fault_state; + +typedef bool(*sc_fault_fn) (struct sc_fault_state * state, void *ptr); + +struct sc_fault_state { + int ncalls; +}; + +/** + * Inject a fault for testing. + * + * The name of the fault must match the expected calls to sc_faulty(). The + * second argument is a callback that is invoked each time sc_faulty() is + * called. It is designed to inspect an argument passed to sc_faulty() and as + * well as the state of the fault injection point and return a boolean + * indicating that a fault has occurred. + * + * After testing faults should be reset using sc_reset_faults(). + **/ + +void sc_break(const char *name, sc_fault_fn fn); + +/** + * Remove all the injected faults. + **/ +void sc_reset_faults(void); + +#endif // ifndef _ENABLE_FAULT_INJECTION + +#endif diff --git a/cmd/libsnap-confine-private/locking-test.c b/cmd/libsnap-confine-private/locking-test.c new file mode 100644 index 00000000..28dd1e72 --- /dev/null +++ b/cmd/libsnap-confine-private/locking-test.c @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "locking.h" +#include "locking.c" + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/test-utils.h" + +#include + +#include +#include + +// Set alternate locking directory +static void sc_set_lock_dir(const char *dir) +{ + sc_lock_dir = dir; +} + +// Use temporary directory for locking. +// +// The directory is automatically reset to the real value at the end of the +// test. +static const char *sc_test_use_fake_lock_dir(void) +{ + char *lock_dir = NULL; + if (g_test_subprocess()) { + // Check if the environment variable is set. If so then someone is already + // managing the temporary directory and we should not create a new one. + lock_dir = getenv("SNAP_CONFINE_LOCK_DIR"); + g_assert_nonnull(lock_dir); + } else { + lock_dir = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(lock_dir); + g_test_queue_free(lock_dir); + g_assert_cmpint(setenv("SNAP_CONFINE_LOCK_DIR", lock_dir, 0), + ==, 0); + g_test_queue_destroy((GDestroyNotify) unsetenv, + "SNAP_CONFINE_LOCK_DIR"); + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, lock_dir); + } + g_test_queue_destroy((GDestroyNotify) sc_set_lock_dir, SC_LOCK_DIR); + sc_set_lock_dir(lock_dir); + return lock_dir; +} + +// Check that locking a namespace actually flock's the mutex with LOCK_EX +static void test_sc_lock_unlock(void) +{ + const char *lock_dir = sc_test_use_fake_lock_dir(); + int fd = sc_lock("foo"); + // Construct the name of the lock file + char *lock_file SC_CLEANUP(sc_cleanup_string) = NULL; + lock_file = g_strdup_printf("%s/foo.lock", lock_dir); + // Open the lock file again to obtain a separate file descriptor. + // According to flock(2) locks are associated with an open file table entry + // so this descriptor will be separate and can compete for the same lock. + int lock_fd SC_CLEANUP(sc_cleanup_close) = -1; + lock_fd = open(lock_file, O_RDWR | O_CLOEXEC | O_NOFOLLOW); + g_assert_cmpint(lock_fd, !=, -1); + // The non-blocking lock operation should fail with EWOULDBLOCK as the lock + // file is locked by sc_nlock_ns_mutex() already. + int err = flock(lock_fd, LOCK_EX | LOCK_NB); + int saved_errno = errno; + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(saved_errno, ==, EWOULDBLOCK); + // Unlock the lock. + sc_unlock("foo", fd); + // Re-attempt the locking operation. This time it should succeed. + err = flock(lock_fd, LOCK_EX | LOCK_NB); + g_assert_cmpint(err, ==, 0); +} + +static void test_sc_enable_sanity_timeout(void) +{ + if (g_test_subprocess()) { + sc_enable_sanity_timeout(); + debug("waiting..."); + usleep(7 * G_USEC_PER_SEC); + debug("woke up"); + sc_disable_sanity_timeout(); + return; + } + g_test_trap_subprocess(NULL, 5 * G_USEC_PER_SEC, + G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_failed(); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/locking/sc_lock_unlock", test_sc_lock_unlock); + g_test_add_func("/locking/sc_enable_sanity_timeout", + test_sc_enable_sanity_timeout); +} diff --git a/cmd/libsnap-confine-private/locking.c b/cmd/libsnap-confine-private/locking.c new file mode 100644 index 00000000..5d499f07 --- /dev/null +++ b/cmd/libsnap-confine-private/locking.c @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "locking.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +/** + * Flag indicating that a sanity timeout has expired. + **/ +static volatile sig_atomic_t sanity_timeout_expired = 0; + +/** + * Signal handler for SIGALRM that sets sanity_timeout_expired flag to 1. + **/ +static void sc_SIGALRM_handler(int signum) +{ + sanity_timeout_expired = 1; +} + +void sc_enable_sanity_timeout(void) +{ + sanity_timeout_expired = 0; + struct sigaction act = {.sa_handler = sc_SIGALRM_handler }; + if (sigemptyset(&act.sa_mask) < 0) { + die("cannot initialize POSIX signal set"); + } + // NOTE: we are using sigaction so that we can explicitly control signal + // flags and *not* pass the SA_RESTART flag. The intent is so that any + // system call we may be sleeping on to gets interrupted. + act.sa_flags = 0; + if (sigaction(SIGALRM, &act, NULL) < 0) { + die("cannot install signal handler for SIGALRM"); + } + alarm(6); + debug("sanity timeout initialized and set for three seconds"); +} + +void sc_disable_sanity_timeout(void) +{ + if (sanity_timeout_expired) { + die("sanity timeout expired"); + } + alarm(0); + struct sigaction act = {.sa_handler = SIG_DFL }; + if (sigemptyset(&act.sa_mask) < 0) { + die("cannot initialize POSIX signal set"); + } + if (sigaction(SIGALRM, &act, NULL) < 0) { + die("cannot uninstall signal handler for SIGALRM"); + } + debug("sanity timeout reset and disabled"); +} + +#define SC_LOCK_DIR "/run/snapd/lock" + +static const char *sc_lock_dir = SC_LOCK_DIR; + +int sc_lock(const char *scope) +{ + // Create (if required) and open the lock directory. + debug("creating lock directory %s (if missing)", sc_lock_dir); + if (sc_nonfatal_mkpath(sc_lock_dir, 0755) < 0) { + die("cannot create lock directory %s", sc_lock_dir); + } + debug("opening lock directory %s", sc_lock_dir); + int dir_fd SC_CLEANUP(sc_cleanup_close) = -1; + dir_fd = + open(sc_lock_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); + if (dir_fd < 0) { + die("cannot open lock directory"); + } + // Construct the name of the lock file. + char lock_fname[PATH_MAX] = { 0 }; + sc_must_snprintf(lock_fname, sizeof lock_fname, "%s/%s.lock", + sc_lock_dir, scope ? : ""); + + // Open the lock file and acquire an exclusive lock. + debug("opening lock file: %s", lock_fname); + int lock_fd = openat(dir_fd, lock_fname, + O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + if (lock_fd < 0) { + die("cannot open lock file: %s", lock_fname); + } + + sc_enable_sanity_timeout(); + debug("acquiring exclusive lock (scope %s)", scope ? : "(global)"); + if (flock(lock_fd, LOCK_EX) < 0) { + sc_disable_sanity_timeout(); + close(lock_fd); + die("cannot acquire exclusive lock (scope %s)", + scope ? : "(global)"); + } else { + sc_disable_sanity_timeout(); + } + return lock_fd; +} + +void sc_unlock(const char *scope, int lock_fd) +{ + // Release the lock and finish. + debug("releasing lock (scope: %s)", scope ? : "(global)"); + if (flock(lock_fd, LOCK_UN) < 0) { + die("cannot release lock (scope: %s)", scope ? : "(global)"); + } + close(lock_fd); +} + +int sc_lock_global(void) +{ + return sc_lock(NULL); +} + +void sc_unlock_global(int lock_fd) +{ + return sc_unlock(NULL, lock_fd); +} diff --git a/cmd/libsnap-confine-private/locking.h b/cmd/libsnap-confine-private/locking.h new file mode 100644 index 00000000..49e77b59 --- /dev/null +++ b/cmd/libsnap-confine-private/locking.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#ifndef SNAP_CONFINE_LOCKING_H +#define SNAP_CONFINE_LOCKING_H + +/** + * Obtain a flock-based, exclusive lock. + * + * The scope may be the name of a snap or NULL (global lock). Each subsequent + * argument is of type sc_locked_fn and gets called with the scope argument. + * The function guarantees that a filesystem lock is reliably acquired and + * released on call to sc_unlock() immediately upon process death. + * + * The actual lock is placed in "/run/snapd/ns" and is either called + * "/run/snapd/ns/.lock" if scope is NULL or + * "/run/snapd/ns/$scope.lock" otherwise. + * + * If the lock cannot be acquired for three seconds (via + * sc_enable_sanity_timeout) then the function fails and the process dies. + * + * The return value needs to be passed to sc_unlock(), there is no need to + * check for errors as the function will die() on any problem. + **/ +int sc_lock(const char *scope); + +/** + * Release a flock-based lock. + * + * This function simply unlocks the lock and closes the file descriptor. + **/ +void sc_unlock(const char *scope, int lock_fd); + +/** + * Obtain a flock-based, exclusive, globally scoped, lock. + * + * This function is exactly like sc_lock(NULL), that is the acquired lock is + * not specific to any snap but global. + **/ +int sc_lock_global(void); + +/** + * Release a flock-based, globally scoped, lock + * + * This function is exactly like sc_unlock(NULL, lock_fd). + **/ +void sc_unlock_global(int lock_fd); + +/** + * Enable a sanity-check timeout. + * + * The timeout is based on good-old alarm(2) and is intended to break a + * suspended system call, such as flock, after a few seconds. The built-in + * timeout is primed for three seconds. After that any sleeping system calls + * are interrupted and a flag is set. + * + * The call should be paired with sc_disable_sanity_check_timeout() that + * disables the alarm and acts on the flag, aborting the process if the timeout + * gets exceeded. + **/ +void sc_enable_sanity_timeout(void); + +/** + * Disable sanity-check timeout and abort the process if it expired. + * + * This call has to be paired with sc_enable_sanity_timeout(), see the function + * description for more details. + **/ +void sc_disable_sanity_timeout(void); + +#endif // SNAP_CONFINE_LOCKING_H diff --git a/cmd/libsnap-confine-private/mount-opt-test.c b/cmd/libsnap-confine-private/mount-opt-test.c new file mode 100644 index 00000000..503950f3 --- /dev/null +++ b/cmd/libsnap-confine-private/mount-opt-test.c @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "mount-opt.h" +#include "mount-opt.c" + +#include +#include + +#include + +static void test_sc_mount_opt2str(void) +{ + char buf[1000] = { 0 }; + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, 0), ==, ""); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_RDONLY), ==, "ro"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOSUID), ==, + "nosuid"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NODEV), ==, + "nodev"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOEXEC), ==, + "noexec"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SYNCHRONOUS), ==, + "sync"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REMOUNT), ==, + "remount"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_MANDLOCK), ==, + "mand"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_DIRSYNC), ==, + "dirsync"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOATIME), ==, + "noatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NODIRATIME), ==, + "nodiratime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_BIND), ==, "bind"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_BIND), ==, + "rbind"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_MOVE), ==, "move"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SILENT), ==, + "silent"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_POSIXACL), ==, + "acl"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_UNBINDABLE), ==, + "unbindable"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_PRIVATE), ==, + "private"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_PRIVATE), + ==, "rprivate"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SLAVE), ==, + "slave"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_SLAVE), + ==, "rslave"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SHARED), ==, + "shared"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_SHARED), + ==, "rshared"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_RELATIME), ==, + "relatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_KERNMOUNT), ==, + "kernmount"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_I_VERSION), ==, + "iversion"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_STRICTATIME), ==, + "strictatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_LAZYTIME), ==, + "lazytime"); + // MS_NOSEC is not defined in userspace + // MS_BORN is not defined in userspace + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_ACTIVE), ==, + "active"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOUSER), ==, + "nouser"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, 0x300), ==, "0x300"); + // random compositions do work + g_assert_cmpstr(sc_mount_opt2str + (buf, sizeof buf, MS_RDONLY | MS_NOEXEC | MS_BIND), ==, + "ro,noexec,bind"); +} + +static void test_sc_mount_cmd(void) +{ + char cmd[10000] = { 0 }; + + // Typical mount + sc_mount_cmd(cmd, sizeof cmd, "/dev/sda3", "/mnt", "ext4", MS_RDONLY, + NULL); + g_assert_cmpstr(cmd, ==, "mount -t ext4 -o ro /dev/sda3 /mnt"); + + // Bind mount + sc_mount_cmd(cmd, sizeof cmd, "/source", "/target", NULL, MS_BIND, + NULL); + g_assert_cmpstr(cmd, ==, "mount --bind /source /target"); + + // + recursive + sc_mount_cmd(cmd, sizeof cmd, "/source", "/target", NULL, + MS_BIND | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --rbind /source /target"); + + // Shared subtree mount + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SHARED, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-shared /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SLAVE, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-slave /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_PRIVATE, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-private /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_UNBINDABLE, + NULL); + g_assert_cmpstr(cmd, ==, "mount --make-unbindable /place"); + + // + recursive + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_SHARED | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rshared /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SLAVE | MS_REC, + NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rslave /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_PRIVATE | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rprivate /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_UNBINDABLE | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-runbindable /place"); + + // Move + sc_mount_cmd(cmd, sizeof cmd, "/from", "/to", NULL, MS_MOVE, NULL); + g_assert_cmpstr(cmd, ==, "mount --move /from /to"); + + // Monster (invalid but let's format it) + char from[PATH_MAX] = { 0 }; + char to[PATH_MAX] = { 0 }; + for (int i = 1; i < PATH_MAX - 1; ++i) { + from[i] = 'a'; + to[i] = 'b'; + } + from[0] = '/'; + to[0] = '/'; + from[PATH_MAX - 1] = 0; + to[PATH_MAX - 1] = 0; + int opts = MS_BIND | MS_MOVE | MS_SHARED | MS_SLAVE | MS_PRIVATE | + MS_UNBINDABLE | MS_REC | MS_RDONLY | MS_NOSUID | MS_NODEV | + MS_NOEXEC | MS_SYNCHRONOUS | MS_REMOUNT | MS_MANDLOCK | MS_DIRSYNC | + MS_NOATIME | MS_NODIRATIME | MS_BIND | MS_SILENT | MS_POSIXACL | + MS_RELATIME | MS_KERNMOUNT | MS_I_VERSION | MS_STRICTATIME | + MS_LAZYTIME; + const char *fstype = "fstype"; + sc_mount_cmd(cmd, sizeof cmd, from, to, fstype, opts, NULL); + const char *expected = + "mount -t fstype " + "--rbind --move --make-rshared --make-rslave --make-rprivate --make-runbindable " + "-o ro,nosuid,nodev,noexec,sync,remount,mand,dirsync,noatime,nodiratime,silent," + "acl,relatime,kernmount,iversion,strictatime,lazytime " + "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + "/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + g_assert_cmpstr(cmd, ==, expected); +} + +static void test_sc_umount_cmd(void) +{ + char cmd[1000] = { 0 }; + + // Typical umount + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", 0); + g_assert_cmpstr(cmd, ==, "umount /mnt/foo"); + + // Force + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_FORCE); + g_assert_cmpstr(cmd, ==, "umount --force /mnt/foo"); + + // Detach + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_DETACH); + g_assert_cmpstr(cmd, ==, "umount --lazy /mnt/foo"); + + // Expire + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_EXPIRE); + g_assert_cmpstr(cmd, ==, "umount --expire /mnt/foo"); + + // O_NOFOLLOW variant for umount + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", UMOUNT_NOFOLLOW); + g_assert_cmpstr(cmd, ==, "umount --no-follow /mnt/foo"); + + // Everything at once + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", + MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW); + g_assert_cmpstr(cmd, ==, + "umount --force --lazy --expire --no-follow /mnt/foo"); +} + +static bool broken_mount(struct sc_fault_state *state, void *ptr) +{ + errno = EACCES; + return true; +} + +static void test_sc_do_mount(void) +{ + if (g_test_subprocess()) { + sc_break("mount", broken_mount); + sc_do_mount("/foo", "/bar", "ext4", MS_RDONLY, NULL); + + g_test_message("expected sc_do_mount not to return"); + sc_reset_faults(); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot perform operation: mount -t ext4 -o ro /foo /bar: Permission denied\n"); +} + +static bool broken_umount(struct sc_fault_state *state, void *ptr) +{ + errno = EACCES; + return true; +} + +static void test_sc_do_umount(void) +{ + if (g_test_subprocess()) { + sc_break("umount", broken_umount); + sc_do_umount("/foo", MNT_DETACH); + + g_test_message("expected sc_do_umount not to return"); + sc_reset_faults(); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot perform operation: umount --lazy /foo: Permission denied\n"); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str); + g_test_add_func("/mount/sc_mount_cmd", test_sc_mount_cmd); + g_test_add_func("/mount/sc_umount_cmd", test_sc_umount_cmd); + g_test_add_func("/mount/sc_do_mount", test_sc_do_mount); + g_test_add_func("/mount/sc_do_umount", test_sc_do_umount); +} diff --git a/cmd/libsnap-confine-private/mount-opt.c b/cmd/libsnap-confine-private/mount-opt.c new file mode 100644 index 00000000..0e3f5a59 --- /dev/null +++ b/cmd/libsnap-confine-private/mount-opt.c @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "mount-opt.h" + +#include +#include +#include +#include +#include +#include + +#include "fault-injection.h" +#include "privs.h" +#include "string-utils.h" +#include "utils.h" + +const char *sc_mount_opt2str(char *buf, size_t buf_size, unsigned long flags) +{ + unsigned long used = 0; + sc_string_init(buf, buf_size); + +#define F(FLAG, TEXT) do { \ + if (flags & (FLAG)) { \ + sc_string_append(buf, buf_size, #TEXT ","); flags ^= (FLAG); \ + } \ + } while (0) + + F(MS_RDONLY, ro); + F(MS_NOSUID, nosuid); + F(MS_NODEV, nodev); + F(MS_NOEXEC, noexec); + F(MS_SYNCHRONOUS, sync); + F(MS_REMOUNT, remount); + F(MS_MANDLOCK, mand); + F(MS_DIRSYNC, dirsync); + F(MS_NOATIME, noatime); + F(MS_NODIRATIME, nodiratime); + if (flags & MS_BIND) { + if (flags & MS_REC) { + sc_string_append(buf, buf_size, "rbind,"); + used |= MS_REC; + } else { + sc_string_append(buf, buf_size, "bind,"); + } + flags ^= MS_BIND; + } + F(MS_MOVE, move); + // The MS_REC flag handled separately by affected flags (MS_BIND, + // MS_PRIVATE, MS_SLAVE, MS_SHARED) + // XXX: kernel has MS_VERBOSE, glibc has MS_SILENT, both use the same constant + F(MS_SILENT, silent); + F(MS_POSIXACL, acl); + F(MS_UNBINDABLE, unbindable); + if (flags & MS_PRIVATE) { + if (flags & MS_REC) { + sc_string_append(buf, buf_size, "rprivate,"); + used |= MS_REC; + } else { + sc_string_append(buf, buf_size, "private,"); + } + flags ^= MS_PRIVATE; + } + if (flags & MS_SLAVE) { + if (flags & MS_REC) { + sc_string_append(buf, buf_size, "rslave,"); + used |= MS_REC; + } else { + sc_string_append(buf, buf_size, "slave,"); + } + flags ^= MS_SLAVE; + } + if (flags & MS_SHARED) { + if (flags & MS_REC) { + sc_string_append(buf, buf_size, "rshared,"); + used |= MS_REC; + } else { + sc_string_append(buf, buf_size, "shared,"); + } + flags ^= MS_SHARED; + } + flags ^= used; // this is just for MS_REC + F(MS_RELATIME, relatime); + F(MS_KERNMOUNT, kernmount); + F(MS_I_VERSION, iversion); + F(MS_STRICTATIME, strictatime); +#ifndef MS_LAZYTIME +#define MS_LAZYTIME (1<<25) +#endif + F(MS_LAZYTIME, lazytime); +#ifndef MS_NOSEC +#define MS_NOSEC (1 << 28) +#endif + F(MS_NOSEC, nosec); +#ifndef MS_BORN +#define MS_BORN (1 << 29) +#endif + F(MS_BORN, born); + F(MS_ACTIVE, active); + F(MS_NOUSER, nouser); +#undef F + // Render any flags that are unaccounted for. + if (flags) { + char of[128] = { 0 }; + sc_must_snprintf(of, sizeof of, "%#lx", flags); + sc_string_append(buf, buf_size, of); + } + // Chop the excess comma from the end. + size_t len = strnlen(buf, buf_size); + if (len > 0 && buf[len - 1] == ',') { + buf[len - 1] = 0; + } + return buf; +} + +const char *sc_mount_cmd(char *buf, size_t buf_size, const char *source, const char + *target, const char *fs_type, unsigned long mountflags, const + void *data) +{ + sc_string_init(buf, buf_size); + sc_string_append(buf, buf_size, "mount"); + + // Add filesysystem type if it's there and doesn't have the special value "none" + if (fs_type != NULL && strncmp(fs_type, "none", 5) != 0) { + sc_string_append(buf, buf_size, " -t "); + sc_string_append(buf, buf_size, fs_type); + } + // Check for some special, dedicated options, that aren't represented with + // the generic mount option argument (mount -o ...), by collecting those + // options that we will display as command line arguments in + // used_special_flags. This is used below to filter out these arguments + // from mount_flags when calling sc_mount_opt2str(). + int used_special_flags = 0; + + // Bind-ounts (bind) + if (mountflags & MS_BIND) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --rbind"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --bind"); + } + used_special_flags |= MS_BIND; + } + // Moving mount point location (move) + if (mountflags & MS_MOVE) { + sc_string_append(buf, buf_size, " --move"); + used_special_flags |= MS_MOVE; + } + // Shared subtree operations (shared, slave, private, unbindable). + if (MS_SHARED & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rshared"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-shared"); + } + used_special_flags |= MS_SHARED; + } + + if (MS_SLAVE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rslave"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-slave"); + } + used_special_flags |= MS_SLAVE; + } + + if (MS_PRIVATE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rprivate"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-private"); + } + used_special_flags |= MS_PRIVATE; + } + + if (MS_UNBINDABLE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-runbindable"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-unbindable"); + } + used_special_flags |= MS_UNBINDABLE; + } + // If regular option syntax exists then use it. + if (mountflags & ~used_special_flags) { + char opts_buf[1000] = { 0 }; + sc_mount_opt2str(opts_buf, sizeof opts_buf, mountflags & + ~used_special_flags); + sc_string_append(buf, buf_size, " -o "); + sc_string_append(buf, buf_size, opts_buf); + } + // Add source and target locations + if (source != NULL && strncmp(source, "none", 5) != 0) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, source); + } + if (target != NULL && strncmp(target, "none", 5) != 0) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, target); + } + + return buf; +} + +const char *sc_umount_cmd(char *buf, size_t buf_size, const char *target, + int flags) +{ + sc_string_init(buf, buf_size); + sc_string_append(buf, buf_size, "umount"); + + if (flags & MNT_FORCE) { + sc_string_append(buf, buf_size, " --force"); + } + + if (flags & MNT_DETACH) { + sc_string_append(buf, buf_size, " --lazy"); + } + if (flags & MNT_EXPIRE) { + // NOTE: there's no real command line option for MNT_EXPIRE + sc_string_append(buf, buf_size, " --expire"); + } + if (flags & UMOUNT_NOFOLLOW) { + // NOTE: there's no real command line option for UMOUNT_NOFOLLOW + sc_string_append(buf, buf_size, " --no-follow"); + } + if (target != NULL) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, target); + } + + return buf; +} + +void sc_do_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data) +{ + char buf[10000] = { 0 }; + const char *mount_cmd = NULL; + + if (sc_is_debug_enabled()) { +#ifdef SNAP_CONFINE_DEBUG_BUILD + mount_cmd = sc_mount_cmd(buf, sizeof(buf), source, + target, fs_type, mountflags, data); +#else + mount_cmd = "(disabled) use debug build to see details"; +#endif + debug("performing operation: %s", mount_cmd); + } + if (sc_faulty("mount", NULL) + || mount(source, target, fs_type, mountflags, data) < 0) { + // Save errno as ensure can clobber it. + int saved_errno = errno; + + // Drop privileges so that we can compute our nice error message + // without risking an attack on one of the string functions there. + sc_privs_drop(); + + // Compute the equivalent mount command. + if (mount_cmd == NULL) { + mount_cmd = sc_mount_cmd(buf, sizeof(buf), source, + target, fs_type, mountflags, + data); + } + // Restore errno and die. + errno = saved_errno; + die("cannot perform operation: %s", mount_cmd); + } +} + +void sc_do_umount(const char *target, int flags) +{ + char buf[10000] = { 0 }; + const char *umount_cmd = NULL; + + if (sc_is_debug_enabled()) { +#ifdef SNAP_CONFINE_DEBUG_BUILD + umount_cmd = sc_umount_cmd(buf, sizeof(buf), target, flags); +#else + umount_cmd = "(disabled) use debug build to see details"; +#endif + debug("performing operation: %s", umount_cmd); + } + if (sc_faulty("umount", NULL) || umount2(target, flags) < 0) { + // Save errno as ensure can clobber it. + int saved_errno = errno; + + // Drop privileges so that we can compute our nice error message + // without risking an attack on one of the string functions there. + sc_privs_drop(); + + // Compute the equivalent umount command. + if (umount_cmd == NULL) { + umount_cmd = sc_umount_cmd(buf, sizeof(buf), + target, flags); + } + // Restore errno and die. + errno = saved_errno; + die("cannot perform operation: %s", umount_cmd); + } +} diff --git a/cmd/libsnap-confine-private/mount-opt.h b/cmd/libsnap-confine-private/mount-opt.h new file mode 100644 index 00000000..d87eefaf --- /dev/null +++ b/cmd/libsnap-confine-private/mount-opt.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_MOUNT_OPT_H +#define SNAP_CONFINE_MOUNT_OPT_H + +#include + +/** + * Convert flags for mount(2) system call to a string representation. + **/ +const char *sc_mount_opt2str(char *buf, size_t buf_size, unsigned long flags); + +/** + * Compute an equivalent mount(8) command from mount(2) arguments. + * + * This function serves as a human-readable representation of the mount system + * call. The return value is a string that looks like a shell mount command. + * + * Note that the returned command is may not be a valid mount command. No + * sanity checking is performed on the mount flags, source or destination + * arguments. + * + * The returned value is always buf, it is provided as a convenience. + **/ +const char *sc_mount_cmd(char *buf, size_t buf_size, const char *source, const char + *target, const char *fs_type, unsigned long mountflags, + const void *data); + +/** + * Compute an equivalent umount(8) command from umount2(2) arguments. + * + * This function serves as a human-readable representation of the unmount + * system call. The return value is a string that looks like a shell unmount + * command. + * + * Note that some flags are not surfaced at umount command line level. For + * those flags a fake option is synthesized. + * + * Note that the returned command is may not be a valid umount command. No + * sanity checking is performed on the mount flags, source or destination + * arguments. + * + * The returned value is always buf, it is provided as a convenience. + **/ +const char *sc_umount_cmd(char *buf, size_t buf_size, const char *target, + int flags); + +/** + * A thin wrapper around mount(2) with logging and error checks. + **/ +void sc_do_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data); + +/** + * A thin wrapper around umount(2) with logging and error checks. + **/ +void sc_do_umount(const char *target, int flags); + +#endif // SNAP_CONFINE_MOUNT_OPT_H diff --git a/cmd/libsnap-confine-private/mountinfo-test.c b/cmd/libsnap-confine-private/mountinfo-test.c new file mode 100644 index 00000000..ed0a6101 --- /dev/null +++ b/cmd/libsnap-confine-private/mountinfo-test.c @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "mountinfo.h" +#include "mountinfo.c" + +#include + +static void test_parse_mountinfo_entry__sysfs(void) +{ + const char *line = + "19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 19); + g_assert_cmpint(entry->parent_id, ==, 25); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 18); + g_assert_cmpstr(entry->root, ==, "/"); + g_assert_cmpstr(entry->mount_dir, ==, "/sys"); + g_assert_cmpstr(entry->mount_opts, ==, + "rw,nosuid,nodev,noexec,relatime"); + g_assert_cmpstr(entry->optional_fields, ==, "shared:7"); + g_assert_cmpstr(entry->fs_type, ==, "sysfs"); + g_assert_cmpstr(entry->mount_source, ==, "sysfs"); + g_assert_cmpstr(entry->super_opts, ==, "rw"); + g_assert_null(entry->next); +} + +// Parse the /run/snapd/ns bind mount (over itself) +// Note that /run is itself a tmpfs mount point. +static void test_parse_mountinfo_entry__snapd_ns(void) +{ + const char *line = + "104 23 0:19 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=99840k,mode=755"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 104); + g_assert_cmpint(entry->parent_id, ==, 23); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 19); + g_assert_cmpstr(entry->root, ==, "/snapd/ns"); + g_assert_cmpstr(entry->mount_dir, ==, "/run/snapd/ns"); + g_assert_cmpstr(entry->mount_opts, ==, "rw,nosuid,noexec,relatime"); + g_assert_cmpstr(entry->optional_fields, ==, ""); + g_assert_cmpstr(entry->fs_type, ==, "tmpfs"); + g_assert_cmpstr(entry->mount_source, ==, "tmpfs"); + g_assert_cmpstr(entry->super_opts, ==, "rw,size=99840k,mode=755"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__snapd_mnt(void) +{ + const char *line = + "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 256); + g_assert_cmpint(entry->parent_id, ==, 104); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 3); + g_assert_cmpstr(entry->root, ==, "mnt:[4026532509]"); + g_assert_cmpstr(entry->mount_dir, ==, "/run/snapd/ns/hello-world.mnt"); + g_assert_cmpstr(entry->mount_opts, ==, "rw"); + g_assert_cmpstr(entry->optional_fields, ==, ""); + g_assert_cmpstr(entry->fs_type, ==, "nsfs"); + g_assert_cmpstr(entry->mount_source, ==, "nsfs"); + g_assert_cmpstr(entry->super_opts, ==, "rw"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__garbage(void) +{ + const char *line = "256 104 0:3"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_null(entry); +} + +static void test_parse_mountinfo_entry__no_tags(void) +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts - fs-type mount-source super-opts"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 1); + g_assert_cmpint(entry->parent_id, ==, 2); + g_assert_cmpint(entry->dev_major, ==, 3); + g_assert_cmpint(entry->dev_minor, ==, 4); + g_assert_cmpstr(entry->root, ==, "root"); + g_assert_cmpstr(entry->mount_dir, ==, "mount-dir"); + g_assert_cmpstr(entry->mount_opts, ==, "mount-opts"); + g_assert_cmpstr(entry->optional_fields, ==, ""); + g_assert_cmpstr(entry->fs_type, ==, "fs-type"); + g_assert_cmpstr(entry->mount_source, ==, "mount-source"); + g_assert_cmpstr(entry->super_opts, ==, "super-opts"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__one_tag(void) +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts tag:1 - fs-type mount-source super-opts"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 1); + g_assert_cmpint(entry->parent_id, ==, 2); + g_assert_cmpint(entry->dev_major, ==, 3); + g_assert_cmpint(entry->dev_minor, ==, 4); + g_assert_cmpstr(entry->root, ==, "root"); + g_assert_cmpstr(entry->mount_dir, ==, "mount-dir"); + g_assert_cmpstr(entry->mount_opts, ==, "mount-opts"); + g_assert_cmpstr(entry->optional_fields, ==, "tag:1"); + g_assert_cmpstr(entry->fs_type, ==, "fs-type"); + g_assert_cmpstr(entry->mount_source, ==, "mount-source"); + g_assert_cmpstr(entry->super_opts, ==, "super-opts"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__many_tags(void) +{ + const char *line = + "1 2 3:4 root mount-dir mount-opts tag:1 tag:2 tag:3 tag:4 - fs-type mount-source super-opts"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 1); + g_assert_cmpint(entry->parent_id, ==, 2); + g_assert_cmpint(entry->dev_major, ==, 3); + g_assert_cmpint(entry->dev_minor, ==, 4); + g_assert_cmpstr(entry->root, ==, "root"); + g_assert_cmpstr(entry->mount_dir, ==, "mount-dir"); + g_assert_cmpstr(entry->mount_opts, ==, "mount-opts"); + g_assert_cmpstr(entry->optional_fields, ==, "tag:1 tag:2 tag:3 tag:4"); + g_assert_cmpstr(entry->fs_type, ==, "fs-type"); + g_assert_cmpstr(entry->mount_source, ==, "mount-source"); + g_assert_cmpstr(entry->super_opts, ==, "super-opts"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__empty_source(void) +{ + const char *line = + "304 301 0:45 / /snap/test-snapd-content-advanced-plug/x1 rw,relatime - tmpfs rw"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 304); + g_assert_cmpint(entry->parent_id, ==, 301); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 45); + g_assert_cmpstr(entry->root, ==, "/"); + g_assert_cmpstr(entry->mount_dir, ==, + "/snap/test-snapd-content-advanced-plug/x1"); + g_assert_cmpstr(entry->mount_opts, ==, "rw,relatime"); + g_assert_cmpstr(entry->optional_fields, ==, ""); + g_assert_cmpstr(entry->fs_type, ==, "tmpfs"); + g_assert_cmpstr(entry->mount_source, ==, ""); + g_assert_cmpstr(entry->super_opts, ==, "rw"); + g_assert_null(entry->next); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/mountinfo/parse_mountinfo_entry/sysfs", + test_parse_mountinfo_entry__sysfs); + g_test_add_func("/mountinfo/parse_mountinfo_entry/snapd-ns", + test_parse_mountinfo_entry__snapd_ns); + g_test_add_func("/mountinfo/parse_mountinfo_entry/snapd-mnt", + test_parse_mountinfo_entry__snapd_mnt); + g_test_add_func("/mountinfo/parse_mountinfo_entry/garbage", + test_parse_mountinfo_entry__garbage); + g_test_add_func("/mountinfo/parse_mountinfo_entry/no_tags", + test_parse_mountinfo_entry__no_tags); + g_test_add_func("/mountinfo/parse_mountinfo_entry/one_tags", + test_parse_mountinfo_entry__one_tag); + g_test_add_func("/mountinfo/parse_mountinfo_entry/many_tags", + test_parse_mountinfo_entry__many_tags); + g_test_add_func + ("/mountinfo/parse_mountinfo_entry/empty_source", + test_parse_mountinfo_entry__empty_source); +} diff --git a/cmd/libsnap-confine-private/mountinfo.c b/cmd/libsnap-confine-private/mountinfo.c new file mode 100644 index 00000000..36f12d57 --- /dev/null +++ b/cmd/libsnap-confine-private/mountinfo.c @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + */ + +#include "mountinfo.h" + +#include +#include +#include +#include + +#include "cleanup-funcs.h" + +/** + * Parse a single mountinfo entry (line). + * + * The format, described by Linux kernel documentation, is as follows: + * + * 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + * (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + * + * (1) mount ID: unique identifier of the mount (may be reused after umount) + * (2) parent ID: ID of parent (or of self for the top of the mount tree) + * (3) major:minor: value of st_dev for files on filesystem + * (4) root: root of the mount within the filesystem + * (5) mount point: mount point relative to the process's root + * (6) mount options: per mount options + * (7) optional fields: zero or more fields of the form "tag[:value]" + * (8) separator: marks the end of the optional fields + * (9) filesystem type: name of filesystem of the form "type[.subtype]" + * (10) mount source: filesystem specific information or "none" + * (11) super options: per super block options + **/ +static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) + __attribute__ ((nonnull(1))); + +/** + * Free a sc_mountinfo structure and all its entries. + **/ +static void sc_free_mountinfo(struct sc_mountinfo *info) + __attribute__ ((nonnull(1))); + +/** + * Free a sc_mountinfo entry. + **/ +static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry) + __attribute__ ((nonnull(1))); + +struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info) +{ + return info->first; +} + +struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry + *entry) +{ + return entry->next; +} + +struct sc_mountinfo *sc_parse_mountinfo(const char *fname) +{ + struct sc_mountinfo *info = calloc(1, sizeof *info); + if (info == NULL) { + return NULL; + } + if (fname == NULL) { + fname = "/proc/self/mountinfo"; + } + FILE *f SC_CLEANUP(sc_cleanup_file) = NULL; + f = fopen(fname, "rt"); + if (f == NULL) { + free(info); + return NULL; + } + char *line SC_CLEANUP(sc_cleanup_string) = NULL; + size_t line_size = 0; + struct sc_mountinfo_entry *entry, *last = NULL; + for (;;) { + errno = 0; + if (getline(&line, &line_size, f) == -1) { + if (errno != 0) { + sc_free_mountinfo(info); + return NULL; + } + break; + }; + entry = sc_parse_mountinfo_entry(line); + if (entry == NULL) { + sc_free_mountinfo(info); + return NULL; + } + if (last != NULL) { + last->next = entry; + } else { + info->first = entry; + } + last = entry; + } + return info; +} + +static void show_buffers(const char *line, int offset, + struct sc_mountinfo_entry *entry) +{ +#ifdef MOUNTINFO_DEBUG + fprintf(stderr, "Input buffer (first), with offset arrow\n"); + fprintf(stderr, "Output buffer (second)\n"); + + fputc(' ', stderr); + for (int i = 0; i < offset - 1; ++i) + fputc('-', stderr); + fputc('v', stderr); + fputc('\n', stderr); + + fprintf(stderr, ">%s<\n", line); + + fputc('>', stderr); + for (int i = 0; i < strlen(line); ++i) { + int c = entry->line_buf[i]; + fputc(c == 0 ? '@' : c == 1 ? '#' : c, stderr); + } + fputc('<', stderr); + fputc('\n', stderr); + + fputc('>', stderr); + for (int i = 0; i < strlen(line); ++i) + fputc('=', stderr); + fputc('<', stderr); + fputc('\n', stderr); +#endif // MOUNTINFO_DEBUG +} + +static char *parse_next_string_field(struct sc_mountinfo_entry *entry, + const char *line, int *offset) +{ + int offset_delta = 0; + char *field = &entry->line_buf[0] + *offset; + if (line[*offset] == ' ') { + // Special case for empty fields which cannot be parsed with %s. + *field = '\0'; + *offset += 1; + } else { + int nscanned = + sscanf(line + *offset, "%s%n", field, &offset_delta); + if (nscanned != 1) + return NULL; + *offset += offset_delta; + if (line[*offset] == ' ') { + *offset += 1; + } + } + show_buffers(line, *offset, entry); + return field; +} + +static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) +{ + // NOTE: the sc_mountinfo structure is allocated along with enough extra + // storage to hold the whole line we are parsing. This is used as backing + // store for all text fields. + // + // The idea is that since the line has a given length and we are only after + // set of substrings we can easily predict the amount of required space + // (after all, it is just a set of non-overlapping substrings) and append + // it to the allocated entry structure. + // + // The parsing code below, specifically parse_next_string_field(), uses + // this extra memory to hold data parsed from the original line. In the + // end, the result is similar to using strtok except that the source and + // destination buffers are separate. + // + // At the end of the parsing process, the input buffer (line) and the + // output buffer (entry->line_buf) are the same except for where spaces + // were converted into NUL bytes (string terminators) and except for the + // leading part of the buffer that contains mount_id, parent_id, dev_major + // and dev_minor integer fields that are parsed separately. + // + // If MOUNTINFO_DEBUG is defined then extra debugging is printed to stderr + // and this allows for visual analysis of what is going on. + struct sc_mountinfo_entry *entry = + calloc(1, sizeof *entry + strlen(line) + 1); + if (entry == NULL) { + return NULL; + } +#ifdef MOUNTINFO_DEBUG + // Poison the buffer with '\1' bytes that are printed as '#' characters + // by show_buffers() below. This is "unaltered" memory. + memset(entry->line_buf, 1, strlen(line)); +#endif // MOUNTINFO_DEBUG + int nscanned; + int offset_delta, offset = 0; + nscanned = sscanf(line, "%d %d %u:%u %n", + &entry->mount_id, &entry->parent_id, + &entry->dev_major, &entry->dev_minor, &offset_delta); + if (nscanned != 4) + goto fail; + offset += offset_delta; + + show_buffers(line, offset, entry); + + if ((entry->root = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + if ((entry->mount_dir = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + if ((entry->mount_opts = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + entry->optional_fields = &entry->line_buf[0] + offset; + // NOTE: This ensures that optional_fields is never NULL. If this changes, + // must adjust all callers of parse_mountinfo_entry() accordingly. + for (int field_num = 0;; ++field_num) { + char *opt_field = parse_next_string_field(entry, line, &offset); + if (opt_field == NULL) + goto fail; + if (strcmp(opt_field, "-") == 0) { + opt_field[0] = 0; + break; + } + if (field_num > 0) { + opt_field[-1] = ' '; + } + } + if ((entry->fs_type = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + if ((entry->mount_source = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + if ((entry->super_opts = + parse_next_string_field(entry, line, &offset)) == NULL) + goto fail; + show_buffers(line, offset, entry); + return entry; + fail: + free(entry); + return NULL; +} + +void sc_cleanup_mountinfo(struct sc_mountinfo **ptr) +{ + if (*ptr != NULL) { + sc_free_mountinfo(*ptr); + *ptr = NULL; + } +} + +static void sc_free_mountinfo(struct sc_mountinfo *info) +{ + struct sc_mountinfo_entry *entry, *next; + for (entry = info->first; entry != NULL; entry = next) { + next = entry->next; + sc_free_mountinfo_entry(entry); + } + free(info); +} + +static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry) +{ + free(entry); +} diff --git a/cmd/libsnap-confine-private/mountinfo.h b/cmd/libsnap-confine-private/mountinfo.h new file mode 100644 index 00000000..6e52c93a --- /dev/null +++ b/cmd/libsnap-confine-private/mountinfo.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + */ + +#ifndef SNAP_CONFINE_MOUNTINFO_H +#define SNAP_CONFINE_MOUNTINFO_H + +/** + * Structure describing entire /proc/self/sc_mountinfo file + **/ +struct sc_mountinfo { + struct sc_mountinfo_entry *first; +}; + +/** + * Structure describing a single entry in /proc/self/sc_mountinfo + **/ +struct sc_mountinfo_entry { + /** + * The mount identifier of a given mount entry. + **/ + int mount_id; + /** + * The parent mount identifier of a given mount entry. + **/ + int parent_id; + unsigned dev_major, dev_minor; + /** + * The root directory of a given mount entry. + **/ + char *root; + /** + * The mount point of a given mount entry. + **/ + char *mount_dir; + /** + * The mount options of a given mount entry. + **/ + char *mount_opts; + /** + * Optional tagged data associated of a given mount entry. + * + * The return value is a string (possibly empty but never NULL) in the format + * tag[:value]. Known tags are: + * + * "shared:X": + * mount is shared in peer group X + * "master:X": + * mount is slave to peer group X + * "propagate_from:X" + * mount is slave and receives propagation from peer group X (*) + * "unbindable": + * mount is unbindable + * + * (*) X is the closest dominant peer group under the process's root. + * If X is the immediate master of the mount, or if there's no dominant peer + * group under the same root, then only the "master:X" field is present and not + * the "propagate_from:X" field. + **/ + char *optional_fields; + /** + * The file system type of a given mount entry. + **/ + char *fs_type; + /** + * The source of a given mount entry. + **/ + char *mount_source; + /** + * The super block options of a given mount entry. + **/ + char *super_opts; + + struct sc_mountinfo_entry *next; + + // Buffer holding all of the text data above. + // + // The buffer must be the last element of the structure. It is allocated + // along with the structure itself and does not need to be freed + // separately. + char line_buf[0]; +}; + +/** + * Parse a file in according to sc_mountinfo syntax. + * + * The argument can be used to parse an arbitrary file. NULL can be used to + * implicitly parse /proc/self/sc_mountinfo, that is the mount information + * associated with the current process. + **/ +struct sc_mountinfo *sc_parse_mountinfo(const char *fname); + +/** + * Free a sc_mountinfo structure. + * + * This function is designed to be used with __attribute__((cleanup)) so it + * takes a pointer to the freed object (which is also a pointer). + **/ +void sc_cleanup_mountinfo(struct sc_mountinfo **ptr) + __attribute__ ((nonnull(1))); + +/** + * Get the first sc_mountinfo entry. + * + * The returned value may be NULL if the parsed file contained no entries. The + * returned value is bound to the lifecycle of the whole sc_mountinfo structure + * and should not be freed explicitly. + **/ +struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info) + __attribute__ ((nonnull(1))); + +/** + * Get the next sc_mountinfo entry. + * + * The returned value is a pointer to the next sc_mountinfo entry or NULL if this + * was the last entry. The returned value is bound to the lifecycle of the + * whole sc_mountinfo structure and should not be freed explicitly. + **/ +struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry + *entry) + __attribute__ ((nonnull(1))); + +#endif diff --git a/cmd/libsnap-confine-private/privs-test.c b/cmd/libsnap-confine-private/privs-test.c new file mode 100644 index 00000000..ac32f681 --- /dev/null +++ b/cmd/libsnap-confine-private/privs-test.c @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "privs.h" +#include "privs.c" + +#include + +// Test that dropping permissions really works +static void test_sc_privs_drop(void) +{ + if (geteuid() != 0 || getuid() == 0) { + g_test_skip("run this test after chown root.root; chmod u+s"); + return; + } + if (getegid() != 0 || getgid() == 0) { + g_test_skip("run this test after chown root.root; chmod g+s"); + return; + } + if (g_test_subprocess()) { + // We start as a regular user with effective-root identity. + g_assert_cmpint(getuid(), !=, 0); + g_assert_cmpint(getgid(), !=, 0); + + g_assert_cmpint(geteuid(), ==, 0); + g_assert_cmpint(getegid(), ==, 0); + + // We drop the privileges. + sc_privs_drop(); + + // The we are no longer root. + g_assert_cmpint(getuid(), !=, 0); + g_assert_cmpint(geteuid(), !=, 0); + g_assert_cmpint(getgid(), !=, 0); + g_assert_cmpint(getegid(), !=, 0); + + // We don't have any supplementary groups. + gid_t groups[2]; + int num_groups = getgroups(1, groups); + g_assert_cmpint(num_groups, ==, 1); + g_assert_cmpint(groups[0], ==, getgid()); + + // All done. + return; + } + g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed(); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/privs/sc_privs_drop", test_sc_privs_drop); +} diff --git a/cmd/libsnap-confine-private/privs.c b/cmd/libsnap-confine-private/privs.c new file mode 100644 index 00000000..18b9587d --- /dev/null +++ b/cmd/libsnap-confine-private/privs.c @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "privs.h" + +#define _GNU_SOURCE + +#include + +#include +#include +#include +#include +#include + +#include "utils.h" + +static bool sc_has_capability(const char *cap_name) +{ + // Lookup capability with the given name. + cap_value_t cap; + if (cap_from_name(cap_name, &cap) < 0) { + die("cannot resolve capability name %s", cap_name); + } + // Get the capability state of the current process. + cap_t caps; + if ((caps = cap_get_proc()) == NULL) { + die("cannot obtain capability state (cap_get_proc)"); + } + // Read the effective value of the flag we're dealing with + cap_flag_value_t cap_flags_value; + if (cap_get_flag(caps, cap, CAP_EFFECTIVE, &cap_flags_value) < 0) { + cap_free(caps); // don't bother checking, we die anyway. + die("cannot obtain value of capability flag (cap_get_flag)"); + } + // Free the representation of the capability state of the current process. + if (cap_free(caps) < 0) { + die("cannot free capability flag (cap_free)"); + } + // Check if the effective bit of the capability is set. + return cap_flags_value == CAP_SET; +} + +void sc_privs_drop(void) +{ + gid_t gid = getgid(); + uid_t uid = getuid(); + + // Drop extra group membership if we can. + if (sc_has_capability("cap_setgid")) { + gid_t gid_list[1] = { gid }; + if (setgroups(1, gid_list) < 0) { + die("cannot set supplementary group identifiers"); + } + } + // Switch to real group ID + if (setgid(getgid()) < 0) { + die("cannot set group identifier to %d", gid); + } + // Switch to real user ID + if (setuid(getuid()) < 0) { + die("cannot set user identifier to %d", uid); + } +} diff --git a/cmd/libsnap-confine-private/privs.h b/cmd/libsnap-confine-private/privs.h new file mode 100644 index 00000000..41a4eb5e --- /dev/null +++ b/cmd/libsnap-confine-private/privs.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_PRIVS_H +#define SNAP_CONFINE_PRIVS_H + +/** + * Permanently drop elevated permissions. + * + * If the user has elevated permission as a result of running a setuid root + * application then such permission are permanently dropped. + * + * The set of dropped permissions include: + * - user and group identifier + * - supplementary group identifiers + * + * The function ensures that the elevated permission are dropped or dies if + * this cannot be achieved. Note that only the elevated permissions are + * dropped. When the process itself was started by root then this function does + * nothing at all. + **/ +void sc_privs_drop(void); + +#endif diff --git a/cmd/libsnap-confine-private/secure-getenv-test.c b/cmd/libsnap-confine-private/secure-getenv-test.c new file mode 100644 index 00000000..61b42502 --- /dev/null +++ b/cmd/libsnap-confine-private/secure-getenv-test.c @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "secure-getenv.h" +#include "secure-getenv.c" + +#include + +// TODO: write some tests diff --git a/cmd/libsnap-confine-private/secure-getenv.c b/cmd/libsnap-confine-private/secure-getenv.c new file mode 100644 index 00000000..cc8b4dc8 --- /dev/null +++ b/cmd/libsnap-confine-private/secure-getenv.c @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "secure-getenv.h" + +#include +#include + +#ifndef HAVE_SECURE_GETENV +char *secure_getenv(const char *name) +{ + unsigned long secure = getauxval(AT_SECURE); + if (secure != 0) { + return NULL; + } + return getenv(name); +} +#endif // ! HAVE_SECURE_GETENV diff --git a/cmd/libsnap-confine-private/secure-getenv.h b/cmd/libsnap-confine-private/secure-getenv.h new file mode 100644 index 00000000..1b139a3a --- /dev/null +++ b/cmd/libsnap-confine-private/secure-getenv.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#ifndef SNAP_CONFINE_SECURE_GETENV_H +#define SNAP_CONFINE_SECURE_GETENV_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#ifndef HAVE_SECURE_GETENV +/** + * Secure version of getenv() + * + * This version returns NULL if the process is running within a secure context. + * This is exactly the same as the GNU extension to the standard library. It is + * only used when glibc is not available. + **/ +char *secure_getenv(const char *name) + __attribute__ ((nonnull(1), warn_unused_result)); +#endif // ! HAVE_SECURE_GETENV + +#endif diff --git a/cmd/libsnap-confine-private/snap-test.c b/cmd/libsnap-confine-private/snap-test.c new file mode 100644 index 00000000..d7ee8ff9 --- /dev/null +++ b/cmd/libsnap-confine-private/snap-test.c @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "snap.h" +#include "snap.c" + +#include + +static void test_verify_security_tag(void) +{ + // First, test the names we know are good + g_assert_true(verify_security_tag("snap.name.app", "name")); + g_assert_true(verify_security_tag + ("snap.network-manager.NetworkManager", + "network-manager")); + g_assert_true(verify_security_tag("snap.f00.bar-baz1", "f00")); + g_assert_true(verify_security_tag("snap.foo.hook.bar", "foo")); + g_assert_true(verify_security_tag("snap.foo.hook.bar-baz", "foo")); + + // Now, test the names we know are bad + g_assert_false(verify_security_tag + ("pkg-foo.bar.0binary-bar+baz", "bar")); + g_assert_false(verify_security_tag("pkg-foo_bar_1.1", "")); + g_assert_false(verify_security_tag("appname/..", "")); + g_assert_false(verify_security_tag("snap", "")); + g_assert_false(verify_security_tag("snap.", "")); + g_assert_false(verify_security_tag("snap.name", "name")); + g_assert_false(verify_security_tag("snap.name.", "name")); + g_assert_false(verify_security_tag("snap.name.app.", "name")); + g_assert_false(verify_security_tag("snap.name.hook.", "name")); + g_assert_false(verify_security_tag("snap!name.app", "!name")); + g_assert_false(verify_security_tag("snap.-name.app", "-name")); + g_assert_false(verify_security_tag("snap.name!app", "name!")); + g_assert_false(verify_security_tag("snap.name.-app", "name")); + g_assert_false(verify_security_tag("snap.name.app!hook.foo", "name")); + g_assert_false(verify_security_tag("snap.name.app.hook!foo", "name")); + g_assert_false(verify_security_tag("snap.name.app.hook.-foo", "name")); + g_assert_false(verify_security_tag("snap.name.app.hook.f00", "name")); + g_assert_false(verify_security_tag("sna.pname.app", "pname")); + g_assert_false(verify_security_tag("snap.n@me.app", "n@me")); + g_assert_false(verify_security_tag("SNAP.name.app", "name")); + g_assert_false(verify_security_tag("snap.Name.app", "Name")); + // This used to be false but it's now allowed. + g_assert_true(verify_security_tag("snap.0name.app", "0name")); + g_assert_false(verify_security_tag("snap.-name.app", "-name")); + g_assert_false(verify_security_tag("snap.name.@app", "name")); + g_assert_false(verify_security_tag(".name.app", "name")); + g_assert_false(verify_security_tag("snap..name.app", ".name")); + g_assert_false(verify_security_tag("snap.name..app", "name.")); + g_assert_false(verify_security_tag("snap.name.app..", "name")); + + // Test names that are both good, but snap name doesn't match security tag + g_assert_false(verify_security_tag("snap.foo.hook.bar", "fo")); + g_assert_false(verify_security_tag("snap.foo.hook.bar", "fooo")); + g_assert_false(verify_security_tag("snap.foo.hook.bar", "snap")); + g_assert_false(verify_security_tag("snap.foo.hook.bar", "bar")); + + // Regression test 12to8 + g_assert_true(verify_security_tag("snap.12to8.128to8", "12to8")); + g_assert_true(verify_security_tag("snap.123test.123test", "123test")); + g_assert_true(verify_security_tag + ("snap.123test.hook.configure", "123test")); + +} + +static void test_sc_snap_name_validate(void) +{ + struct sc_error *err = NULL; + + // Smoke test, a valid snap name + sc_snap_name_validate("hello-world", &err); + g_assert_null(err); + + // Smoke test: invalid character + sc_snap_name_validate("hello world", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must use lower case letters, digits or dashes"); + sc_error_free(err); + + // Smoke test: no letters + sc_snap_name_validate("", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name must contain at least one letter"); + sc_error_free(err); + + // Smoke test: leading dash + sc_snap_name_validate("-foo", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot start with a dash"); + sc_error_free(err); + + // Smoke test: trailing dash + sc_snap_name_validate("foo-", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot end with a dash"); + sc_error_free(err); + + // Smoke test: double dash + sc_snap_name_validate("f--oo", &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, + "snap name cannot contain two consecutive dashes"); + sc_error_free(err); + + // Smoke test: NULL name is not valid + sc_snap_name_validate(NULL, &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + g_assert_cmpstr(sc_error_msg(err), ==, "snap name cannot be NULL"); + sc_error_free(err); + + const char *valid_names[] = { + "a", "aa", "aaa", "aaaa", + "a-a", "aa-a", "a-aa", "a-b-c", + "a0", "a-0", "a-0a", + "01game", "1-or-2" + }; + for (size_t i = 0; i < sizeof valid_names / sizeof *valid_names; ++i) { + g_test_message("checking valid snap name: %s", valid_names[i]); + sc_snap_name_validate(valid_names[i], &err); + g_assert_null(err); + } + const char *invalid_names[] = { + // name cannot be empty + "", + // dashes alone are not a name + "-", "--", + // double dashes in a name are not allowed + "a--a", + // name should not end with a dash + "a-", + // name cannot have any spaces in it + "a ", " a", "a a", + // a number alone is not a name + "0", "123", + // identifier must be plain ASCII + "日本語", "한글", "ру́сский язы́к", + }; + for (size_t i = 0; i < sizeof invalid_names / sizeof *invalid_names; + ++i) { + g_test_message("checking invalid snap name: >%s<", + invalid_names[i]); + sc_snap_name_validate(invalid_names[i], &err); + g_assert_nonnull(err); + g_assert_true(sc_error_match + (err, SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME)); + sc_error_free(err); + } + // Regression test: 12to8 and 123test + sc_snap_name_validate("12to8", &err); + g_assert_null(err); + sc_snap_name_validate("123test", &err); + g_assert_null(err); + +} + +static void test_sc_snap_name_validate__respects_error_protocol(void) +{ + if (g_test_subprocess()) { + sc_snap_name_validate("hello world", NULL); + g_test_message("expected sc_snap_name_validate to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("snap name must use lower case letters, digits or dashes\n"); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/snap/verify_security_tag", test_verify_security_tag); + g_test_add_func("/snap/sc_snap_name_validate", + test_sc_snap_name_validate); + g_test_add_func("/snap/sc_snap_name_validate/respects_error_protocol", + test_sc_snap_name_validate__respects_error_protocol); +} diff --git a/cmd/libsnap-confine-private/snap.c b/cmd/libsnap-confine-private/snap.c new file mode 100644 index 00000000..f3d23dfb --- /dev/null +++ b/cmd/libsnap-confine-private/snap.c @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "config.h" +#include "snap.h" + +#include +#include +#include +#include +#include + +#include "utils.h" +#include "string-utils.h" +#include "cleanup-funcs.h" + +bool verify_security_tag(const char *security_tag, const char *snap_name) +{ + const char *whitelist_re = + "^snap\\.([a-z0-9](-?[a-z0-9])*)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$"; + regex_t re; + if (regcomp(&re, whitelist_re, REG_EXTENDED) != 0) + die("can not compile regex %s", whitelist_re); + + // first capture is for verifying the full security tag, second capture + // for verifying the snap_name is correct for this security tag + regmatch_t matches[2]; + int status = + regexec(&re, security_tag, sizeof matches / sizeof *matches, + matches, 0); + regfree(&re); + + // Fail if no match or if snap name wasn't captured in the 2nd match group + if (status != 0 || matches[1].rm_so < 0) { + return false; + } + + size_t len = matches[1].rm_eo - matches[1].rm_so; + return len == strlen(snap_name) + && strncmp(security_tag + matches[1].rm_so, snap_name, len) == 0; +} + +bool sc_is_hook_security_tag(const char *security_tag) +{ + const char *whitelist_re = + "^snap\\.[a-z](-?[a-z0-9])*\\.(hook\\.[a-z](-?[a-z])*)$"; + + regex_t re; + if (regcomp(&re, whitelist_re, REG_EXTENDED | REG_NOSUB) != 0) + die("can not compile regex %s", whitelist_re); + + int status = regexec(&re, security_tag, 0, NULL, 0); + regfree(&re); + + return status == 0; +} + +static int skip_lowercase_letters(const char **p) +{ + int skipped = 0; + for (const char *c = *p; *c >= 'a' && *c <= 'z'; ++c) { + skipped += 1; + } + *p = (*p) + skipped; + return skipped; +} + +static int skip_digits(const char **p) +{ + int skipped = 0; + for (const char *c = *p; *c >= '0' && *c <= '9'; ++c) { + skipped += 1; + } + *p = (*p) + skipped; + return skipped; +} + +static int skip_one_char(const char **p, char c) +{ + if (**p == c) { + *p += 1; + return 1; + } + return 0; +} + +void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp) +{ + // NOTE: This function should be synchronized with the two other + // implementations: validate_snap_name and snap.ValidateName. + struct sc_error *err = NULL; + + // Ensure that name is not NULL + if (snap_name == NULL) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name cannot be NULL"); + goto out; + } + // This is a regexp-free routine hand-codes the following pattern: + // + // "^([a-z0-9]+-?)*[a-z](-?[a-z0-9])*$" + // + // The only motivation for not using regular expressions is so that we + // don't run untrusted input against a potentially complex regular + // expression engine. + const char *p = snap_name; + if (skip_one_char(&p, '-')) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name cannot start with a dash"); + goto out; + } + bool got_letter = false; + for (; *p != '\0';) { + if (skip_lowercase_letters(&p) > 0) { + got_letter = true; + continue; + } + if (skip_digits(&p) > 0) { + continue; + } + if (skip_one_char(&p, '-') > 0) { + if (*p == '\0') { + err = + sc_error_init(SC_SNAP_DOMAIN, + SC_SNAP_INVALID_NAME, + "snap name cannot end with a dash"); + goto out; + } + if (skip_one_char(&p, '-') > 0) { + err = + sc_error_init(SC_SNAP_DOMAIN, + SC_SNAP_INVALID_NAME, + "snap name cannot contain two consecutive dashes"); + goto out; + } + continue; + } + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must use lower case letters, digits or dashes"); + goto out; + } + if (!got_letter) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must contain at least one letter"); + } + + out: + sc_error_forward(errorp, err); +} diff --git a/cmd/libsnap-confine-private/snap.h b/cmd/libsnap-confine-private/snap.h new file mode 100644 index 00000000..7251721b --- /dev/null +++ b/cmd/libsnap-confine-private/snap.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_SNAP_H +#define SNAP_CONFINE_SNAP_H + +#include + +#include "error.h" + +/** + * Error domain for errors related to the snap module. + **/ +#define SC_SNAP_DOMAIN "snap" + +enum { + /** The name of the snap is not valid. */ + SC_SNAP_INVALID_NAME = 1, +}; + +/** + * Validate the given snap name. + * + * Valid name cannot be NULL and must match a regular expression describing the + * strict naming requirements. Please refer to snapd source code for details. + * + * The error protocol is observed so if the caller doesn't provide an outgoing + * error pointer the function will die on any error. + **/ +void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp); + +/** + * Validate security tag against strict naming requirements and snap name. + * + * The executable name is of form: + * snap..(|hook.) + * - must start with lowercase letter, then may contain + * lowercase alphanumerics and '-'; it must match snap_name + * - may contain alphanumerics and '-' + * - . + * + */ + +#include "string-utils.h" +#include "string-utils.c" + +#include + +static void test_sc_streq(void) +{ + g_assert_false(sc_streq(NULL, NULL)); + g_assert_false(sc_streq(NULL, "text")); + g_assert_false(sc_streq("text", NULL)); + g_assert_false(sc_streq("foo", "bar")); + g_assert_false(sc_streq("foo", "barbar")); + g_assert_false(sc_streq("foofoo", "bar")); + g_assert_true(sc_streq("text", "text")); + g_assert_true(sc_streq("", "")); +} + +static void test_sc_endswith(void) +{ + // NULL doesn't end with anything, nothing ends with NULL + g_assert_false(sc_endswith("", NULL)); + g_assert_false(sc_endswith(NULL, "")); + g_assert_false(sc_endswith(NULL, NULL)); + // Empty string ends with an empty string + g_assert_true(sc_endswith("", "")); + // Ends-with (matches) + g_assert_true(sc_endswith("foobar", "bar")); + g_assert_true(sc_endswith("foobar", "ar")); + g_assert_true(sc_endswith("foobar", "r")); + g_assert_true(sc_endswith("foobar", "")); + g_assert_true(sc_endswith("bar", "bar")); + // Ends-with (non-matches) + g_assert_false(sc_endswith("foobar", "quux")); + g_assert_false(sc_endswith("", "bar")); + g_assert_false(sc_endswith("b", "bar")); + g_assert_false(sc_endswith("ba", "bar")); +} + +static void test_sc_must_snprintf(void) +{ + char buf[5] = { 0 }; + sc_must_snprintf(buf, sizeof buf, "1234"); + g_assert_cmpstr(buf, ==, "1234"); +} + +static void test_sc_must_snprintf__fail(void) +{ + if (g_test_subprocess()) { + char buf[5]; + sc_must_snprintf(buf, sizeof buf, "12345"); + g_test_message("expected sc_must_snprintf not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot format string: 1234\n"); +} + +// Check that appending to a buffer works OK. +static void test_sc_string_append(void) +{ + union { + char bigbuf[6]; + struct { + signed char canary1; + char buf[4]; + signed char canary2; + }; + } data = { + .buf = { + 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0,}; + + // Sanity check, ensure that the layout of structures is as spelled above. + // (first canary1, then buf and finally canary2. + g_assert_cmpint(((char *)&data.buf[0]) - ((char *)&data.canary1), ==, + 1); + g_assert_cmpint(((char *)&data.buf[4]) - ((char *)&data.canary2), ==, + 0); + + sc_string_append(data.buf, sizeof data.buf, "oo"); + + // Check that we didn't corrupt either canary. + g_assert_cmpint(data.canary1, ==, ~0); + g_assert_cmpint(data.canary2, ==, ~0); + + // Check that we got the result that was expected. + g_assert_cmpstr(data.buf, ==, "foo"); +} + +// Check that appending an empty string to a full buffer is valid. +static void test_sc_string_append__empty_to_full(void) +{ + union { + char bigbuf[6]; + struct { + signed char canary1; + char buf[4]; + signed char canary2; + }; + } data = { + .buf = { + 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0,}; + + // Sanity check, ensure that the layout of structures is as spelled above. + // (first canary1, then buf and finally canary2. + g_assert_cmpint(((char *)&data.buf[0]) - ((char *)&data.canary1), ==, + 1); + g_assert_cmpint(((char *)&data.buf[4]) - ((char *)&data.canary2), ==, + 0); + + sc_string_append(data.buf, sizeof data.buf, ""); + + // Check that we didn't corrupt either canary. + g_assert_cmpint(data.canary1, ==, ~0); + g_assert_cmpint(data.canary2, ==, ~0); + + // Check that we got the result that was expected. + g_assert_cmpstr(data.buf, ==, "foo"); +} + +// Check that the overflow detection works. +static void test_sc_string_append__overflow(void) +{ + if (g_test_subprocess()) { + char buf[4] = { 0 }; + + // Try to append a string that's one character too long. + sc_string_append(buf, sizeof buf, "1234"); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append string: str is too long or unterminated\n"); +} + +// Check that the uninitialized buffer detection works. +static void test_sc_string_append__uninitialized_buf(void) +{ + if (g_test_subprocess()) { + char buf[4] = { 0xFF, 0xFF, 0xFF, 0xFF }; + + // Try to append a string to a buffer which is not a valic C-string. + sc_string_append(buf, sizeof buf, ""); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append string: dst is unterminated\n"); +} + +// Check that `buf' cannot be NULL. +static void test_sc_string_append__NULL_buf(void) +{ + if (g_test_subprocess()) { + char buf[4]; + + sc_string_append(NULL, sizeof buf, "foo"); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append string: buffer is NULL\n"); +} + +// Check that `src' cannot be NULL. +static void test_sc_string_append__NULL_str(void) +{ + if (g_test_subprocess()) { + char buf[4]; + + sc_string_append(buf, sizeof buf, NULL); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append string: string is NULL\n"); +} + +static void test_sc_string_init__normal(void) +{ + char buf[1] = { 0xFF }; + + sc_string_init(buf, sizeof buf); + g_assert_cmpint(buf[0], ==, 0); +} + +static void test_sc_string_init__empty_buf(void) +{ + if (g_test_subprocess()) { + char buf[1] = { 0xFF }; + + sc_string_init(buf, 0); + + g_test_message("expected sc_string_init not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot initialize string, buffer is too small\n"); +} + +static void test_sc_string_init__NULL_buf(void) +{ + if (g_test_subprocess()) { + sc_string_init(NULL, 1); + + g_test_message("expected sc_string_init not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot initialize string, buffer is NULL\n"); +} + +static void test_sc_string_append_char__uninitialized_buf(void) +{ + if (g_test_subprocess()) { + char buf[2] = { 0xFF, 0xFF }; + sc_string_append_char(buf, sizeof buf, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: dst is unterminated\n"); +} + +static void test_sc_string_append_char__NULL_buf(void) +{ + if (g_test_subprocess()) { + sc_string_append_char(NULL, 2, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append character: buffer is NULL\n"); +} + +static void test_sc_string_append_char__overflow(void) +{ + if (g_test_subprocess()) { + char buf[1] = { 0 }; + sc_string_append_char(buf, sizeof buf, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: not enough space\n"); +} + +static void test_sc_string_append_char__invalid_zero(void) +{ + if (g_test_subprocess()) { + char buf[2] = { 0 }; + sc_string_append_char(buf, sizeof buf, '\0'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: cannot append string terminator\n"); +} + +static void test_sc_string_append_char__normal(void) +{ + char buf[16]; + size_t len; + sc_string_init(buf, sizeof buf); + + len = sc_string_append_char(buf, sizeof buf, 'h'); + g_assert_cmpstr(buf, ==, "h"); + g_assert_cmpint(len, ==, 1); + len = sc_string_append_char(buf, sizeof buf, 'e'); + g_assert_cmpstr(buf, ==, "he"); + g_assert_cmpint(len, ==, 2); + len = sc_string_append_char(buf, sizeof buf, 'l'); + g_assert_cmpstr(buf, ==, "hel"); + g_assert_cmpint(len, ==, 3); + len = sc_string_append_char(buf, sizeof buf, 'l'); + g_assert_cmpstr(buf, ==, "hell"); + g_assert_cmpint(len, ==, 4); + len = sc_string_append_char(buf, sizeof buf, 'o'); + g_assert_cmpstr(buf, ==, "hello"); + g_assert_cmpint(len, ==, 5); +} + +static void test_sc_string_append_char_pair__uninitialized_buf(void) +{ + if (g_test_subprocess()) { + char buf[3] = { 0xFF, 0xFF, 0xFF }; + sc_string_append_char_pair(buf, sizeof buf, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: dst is unterminated\n"); +} + +static void test_sc_string_append_char_pair__NULL_buf(void) +{ + if (g_test_subprocess()) { + sc_string_append_char_pair(NULL, 3, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: buffer is NULL\n"); +} + +static void test_sc_string_append_char_pair__overflow(void) +{ + if (g_test_subprocess()) { + char buf[2] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: not enough space\n"); +} + +static void test_sc_string_append_char_pair__invalid_zero_c1(void) +{ + if (g_test_subprocess()) { + char buf[3] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, '\0', 'a'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: cannot append string terminator\n"); +} + +static void test_sc_string_append_char_pair__invalid_zero_c2(void) +{ + if (g_test_subprocess()) { + char buf[3] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, 'a', '\0'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: cannot append string terminator\n"); +} + +static void test_sc_string_append_char_pair__normal(void) +{ + char buf[16]; + size_t len; + sc_string_init(buf, sizeof buf); + + len = sc_string_append_char_pair(buf, sizeof buf, 'h', 'e'); + g_assert_cmpstr(buf, ==, "he"); + g_assert_cmpint(len, ==, 2); + len = sc_string_append_char_pair(buf, sizeof buf, 'l', 'l'); + g_assert_cmpstr(buf, ==, "hell"); + g_assert_cmpint(len, ==, 4); + len = sc_string_append_char_pair(buf, sizeof buf, 'o', '!'); + g_assert_cmpstr(buf, ==, "hello!"); + g_assert_cmpint(len, ==, 6); +} + +static void test_sc_string_quote_NULL_str(void) +{ + if (g_test_subprocess()) { + char buf[16] = { 0 }; + sc_string_quote(buf, sizeof buf, NULL); + + g_test_message("expected sc_string_quote not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot quote string: string is NULL\n"); +} + +static void test_quoting_of(bool tested[], int c, const char *expected) +{ + char buf[16]; + + g_assert_cmpint(c, >=, 0); + g_assert_cmpint(c, <=, 255); + + // Create an input string with one character. + char input[2] = { (unsigned char)c, 0 }; + sc_string_quote(buf, sizeof buf, input); + + // Ensure it was quoted as we expected. + g_assert_cmpstr(buf, ==, expected); + + tested[c] = true; +} + +static void test_sc_string_quote(void) +{ +#define DQ "\"" + char buf[16]; + bool is_tested[256] = { false }; + + // Exhaustive test for quoting of every 8bit input. This is very verbose + // but the goal is to have a very obvious and correct test that ensures no + // edge case is lost. + // + // block 1: 0x00 - 0x0f + test_quoting_of(is_tested, 0x00, DQ "" DQ); + test_quoting_of(is_tested, 0x01, DQ "\\x01" DQ); + test_quoting_of(is_tested, 0x02, DQ "\\x02" DQ); + test_quoting_of(is_tested, 0x03, DQ "\\x03" DQ); + test_quoting_of(is_tested, 0x04, DQ "\\x04" DQ); + test_quoting_of(is_tested, 0x05, DQ "\\x05" DQ); + test_quoting_of(is_tested, 0x06, DQ "\\x06" DQ); + test_quoting_of(is_tested, 0x07, DQ "\\x07" DQ); + test_quoting_of(is_tested, 0x08, DQ "\\x08" DQ); + test_quoting_of(is_tested, 0x09, DQ "\\t" DQ); + test_quoting_of(is_tested, 0x0a, DQ "\\n" DQ); + test_quoting_of(is_tested, 0x0b, DQ "\\v" DQ); + test_quoting_of(is_tested, 0x0c, DQ "\\x0c" DQ); + test_quoting_of(is_tested, 0x0d, DQ "\\r" DQ); + test_quoting_of(is_tested, 0x0e, DQ "\\x0e" DQ); + test_quoting_of(is_tested, 0x0f, DQ "\\x0f" DQ); + // block 2: 0x10 - 0x1f + test_quoting_of(is_tested, 0x10, DQ "\\x10" DQ); + test_quoting_of(is_tested, 0x11, DQ "\\x11" DQ); + test_quoting_of(is_tested, 0x12, DQ "\\x12" DQ); + test_quoting_of(is_tested, 0x13, DQ "\\x13" DQ); + test_quoting_of(is_tested, 0x14, DQ "\\x14" DQ); + test_quoting_of(is_tested, 0x15, DQ "\\x15" DQ); + test_quoting_of(is_tested, 0x16, DQ "\\x16" DQ); + test_quoting_of(is_tested, 0x17, DQ "\\x17" DQ); + test_quoting_of(is_tested, 0x18, DQ "\\x18" DQ); + test_quoting_of(is_tested, 0x19, DQ "\\x19" DQ); + test_quoting_of(is_tested, 0x1a, DQ "\\x1a" DQ); + test_quoting_of(is_tested, 0x1b, DQ "\\x1b" DQ); + test_quoting_of(is_tested, 0x1c, DQ "\\x1c" DQ); + test_quoting_of(is_tested, 0x1d, DQ "\\x1d" DQ); + test_quoting_of(is_tested, 0x1e, DQ "\\x1e" DQ); + test_quoting_of(is_tested, 0x1f, DQ "\\x1f" DQ); + // block 3: 0x20 - 0x2f + test_quoting_of(is_tested, 0x20, DQ " " DQ); + test_quoting_of(is_tested, 0x21, DQ "!" DQ); + test_quoting_of(is_tested, 0x22, DQ "\\\"" DQ); + test_quoting_of(is_tested, 0x23, DQ "#" DQ); + test_quoting_of(is_tested, 0x24, DQ "$" DQ); + test_quoting_of(is_tested, 0x25, DQ "%" DQ); + test_quoting_of(is_tested, 0x26, DQ "&" DQ); + test_quoting_of(is_tested, 0x27, DQ "'" DQ); + test_quoting_of(is_tested, 0x28, DQ "(" DQ); + test_quoting_of(is_tested, 0x29, DQ ")" DQ); + test_quoting_of(is_tested, 0x2a, DQ "*" DQ); + test_quoting_of(is_tested, 0x2b, DQ "+" DQ); + test_quoting_of(is_tested, 0x2c, DQ "," DQ); + test_quoting_of(is_tested, 0x2d, DQ "-" DQ); + test_quoting_of(is_tested, 0x2e, DQ "." DQ); + test_quoting_of(is_tested, 0x2f, DQ "/" DQ); + // block 4: 0x30 - 0x3f + test_quoting_of(is_tested, 0x30, DQ "0" DQ); + test_quoting_of(is_tested, 0x31, DQ "1" DQ); + test_quoting_of(is_tested, 0x32, DQ "2" DQ); + test_quoting_of(is_tested, 0x33, DQ "3" DQ); + test_quoting_of(is_tested, 0x34, DQ "4" DQ); + test_quoting_of(is_tested, 0x35, DQ "5" DQ); + test_quoting_of(is_tested, 0x36, DQ "6" DQ); + test_quoting_of(is_tested, 0x37, DQ "7" DQ); + test_quoting_of(is_tested, 0x38, DQ "8" DQ); + test_quoting_of(is_tested, 0x39, DQ "9" DQ); + test_quoting_of(is_tested, 0x3a, DQ ":" DQ); + test_quoting_of(is_tested, 0x3b, DQ ";" DQ); + test_quoting_of(is_tested, 0x3c, DQ "<" DQ); + test_quoting_of(is_tested, 0x3d, DQ "=" DQ); + test_quoting_of(is_tested, 0x3e, DQ ">" DQ); + test_quoting_of(is_tested, 0x3f, DQ "?" DQ); + // block 5: 0x40 - 0x4f + test_quoting_of(is_tested, 0x40, DQ "@" DQ); + test_quoting_of(is_tested, 0x41, DQ "A" DQ); + test_quoting_of(is_tested, 0x42, DQ "B" DQ); + test_quoting_of(is_tested, 0x43, DQ "C" DQ); + test_quoting_of(is_tested, 0x44, DQ "D" DQ); + test_quoting_of(is_tested, 0x45, DQ "E" DQ); + test_quoting_of(is_tested, 0x46, DQ "F" DQ); + test_quoting_of(is_tested, 0x47, DQ "G" DQ); + test_quoting_of(is_tested, 0x48, DQ "H" DQ); + test_quoting_of(is_tested, 0x49, DQ "I" DQ); + test_quoting_of(is_tested, 0x4a, DQ "J" DQ); + test_quoting_of(is_tested, 0x4b, DQ "K" DQ); + test_quoting_of(is_tested, 0x4c, DQ "L" DQ); + test_quoting_of(is_tested, 0x4d, DQ "M" DQ); + test_quoting_of(is_tested, 0x4e, DQ "N" DQ); + test_quoting_of(is_tested, 0x4f, DQ "O" DQ); + // block 6: 0x50 - 0x5f + test_quoting_of(is_tested, 0x50, DQ "P" DQ); + test_quoting_of(is_tested, 0x51, DQ "Q" DQ); + test_quoting_of(is_tested, 0x52, DQ "R" DQ); + test_quoting_of(is_tested, 0x53, DQ "S" DQ); + test_quoting_of(is_tested, 0x54, DQ "T" DQ); + test_quoting_of(is_tested, 0x55, DQ "U" DQ); + test_quoting_of(is_tested, 0x56, DQ "V" DQ); + test_quoting_of(is_tested, 0x57, DQ "W" DQ); + test_quoting_of(is_tested, 0x58, DQ "X" DQ); + test_quoting_of(is_tested, 0x59, DQ "Y" DQ); + test_quoting_of(is_tested, 0x5a, DQ "Z" DQ); + test_quoting_of(is_tested, 0x5b, DQ "[" DQ); + test_quoting_of(is_tested, 0x5c, DQ "\\\\" DQ); + test_quoting_of(is_tested, 0x5d, DQ "]" DQ); + test_quoting_of(is_tested, 0x5e, DQ "^" DQ); + test_quoting_of(is_tested, 0x5f, DQ "_" DQ); + // block 7: 0x60 - 0x6f + test_quoting_of(is_tested, 0x60, DQ "`" DQ); + test_quoting_of(is_tested, 0x61, DQ "a" DQ); + test_quoting_of(is_tested, 0x62, DQ "b" DQ); + test_quoting_of(is_tested, 0x63, DQ "c" DQ); + test_quoting_of(is_tested, 0x64, DQ "d" DQ); + test_quoting_of(is_tested, 0x65, DQ "e" DQ); + test_quoting_of(is_tested, 0x66, DQ "f" DQ); + test_quoting_of(is_tested, 0x67, DQ "g" DQ); + test_quoting_of(is_tested, 0x68, DQ "h" DQ); + test_quoting_of(is_tested, 0x69, DQ "i" DQ); + test_quoting_of(is_tested, 0x6a, DQ "j" DQ); + test_quoting_of(is_tested, 0x6b, DQ "k" DQ); + test_quoting_of(is_tested, 0x6c, DQ "l" DQ); + test_quoting_of(is_tested, 0x6d, DQ "m" DQ); + test_quoting_of(is_tested, 0x6e, DQ "n" DQ); + test_quoting_of(is_tested, 0x6f, DQ "o" DQ); + // block 8: 0x70 - 0x7f + test_quoting_of(is_tested, 0x70, DQ "p" DQ); + test_quoting_of(is_tested, 0x71, DQ "q" DQ); + test_quoting_of(is_tested, 0x72, DQ "r" DQ); + test_quoting_of(is_tested, 0x73, DQ "s" DQ); + test_quoting_of(is_tested, 0x74, DQ "t" DQ); + test_quoting_of(is_tested, 0x75, DQ "u" DQ); + test_quoting_of(is_tested, 0x76, DQ "v" DQ); + test_quoting_of(is_tested, 0x77, DQ "w" DQ); + test_quoting_of(is_tested, 0x78, DQ "x" DQ); + test_quoting_of(is_tested, 0x79, DQ "y" DQ); + test_quoting_of(is_tested, 0x7a, DQ "z" DQ); + test_quoting_of(is_tested, 0x7b, DQ "{" DQ); + test_quoting_of(is_tested, 0x7c, DQ "|" DQ); + test_quoting_of(is_tested, 0x7d, DQ "}" DQ); + test_quoting_of(is_tested, 0x7e, DQ "~" DQ); + test_quoting_of(is_tested, 0x7f, DQ "\\x7f" DQ); + // block 9 (8-bit): 0x80 - 0x8f + test_quoting_of(is_tested, 0x80, DQ "\\x80" DQ); + test_quoting_of(is_tested, 0x81, DQ "\\x81" DQ); + test_quoting_of(is_tested, 0x82, DQ "\\x82" DQ); + test_quoting_of(is_tested, 0x83, DQ "\\x83" DQ); + test_quoting_of(is_tested, 0x84, DQ "\\x84" DQ); + test_quoting_of(is_tested, 0x85, DQ "\\x85" DQ); + test_quoting_of(is_tested, 0x86, DQ "\\x86" DQ); + test_quoting_of(is_tested, 0x87, DQ "\\x87" DQ); + test_quoting_of(is_tested, 0x88, DQ "\\x88" DQ); + test_quoting_of(is_tested, 0x89, DQ "\\x89" DQ); + test_quoting_of(is_tested, 0x8a, DQ "\\x8a" DQ); + test_quoting_of(is_tested, 0x8b, DQ "\\x8b" DQ); + test_quoting_of(is_tested, 0x8c, DQ "\\x8c" DQ); + test_quoting_of(is_tested, 0x8d, DQ "\\x8d" DQ); + test_quoting_of(is_tested, 0x8e, DQ "\\x8e" DQ); + test_quoting_of(is_tested, 0x8f, DQ "\\x8f" DQ); + // block 10 (8-bit): 0x90 - 0x9f + test_quoting_of(is_tested, 0x90, DQ "\\x90" DQ); + test_quoting_of(is_tested, 0x91, DQ "\\x91" DQ); + test_quoting_of(is_tested, 0x92, DQ "\\x92" DQ); + test_quoting_of(is_tested, 0x93, DQ "\\x93" DQ); + test_quoting_of(is_tested, 0x94, DQ "\\x94" DQ); + test_quoting_of(is_tested, 0x95, DQ "\\x95" DQ); + test_quoting_of(is_tested, 0x96, DQ "\\x96" DQ); + test_quoting_of(is_tested, 0x97, DQ "\\x97" DQ); + test_quoting_of(is_tested, 0x98, DQ "\\x98" DQ); + test_quoting_of(is_tested, 0x99, DQ "\\x99" DQ); + test_quoting_of(is_tested, 0x9a, DQ "\\x9a" DQ); + test_quoting_of(is_tested, 0x9b, DQ "\\x9b" DQ); + test_quoting_of(is_tested, 0x9c, DQ "\\x9c" DQ); + test_quoting_of(is_tested, 0x9d, DQ "\\x9d" DQ); + test_quoting_of(is_tested, 0x9e, DQ "\\x9e" DQ); + test_quoting_of(is_tested, 0x9f, DQ "\\x9f" DQ); + // block 11 (8-bit): 0xa0 - 0xaf + test_quoting_of(is_tested, 0xa0, DQ "\\xa0" DQ); + test_quoting_of(is_tested, 0xa1, DQ "\\xa1" DQ); + test_quoting_of(is_tested, 0xa2, DQ "\\xa2" DQ); + test_quoting_of(is_tested, 0xa3, DQ "\\xa3" DQ); + test_quoting_of(is_tested, 0xa4, DQ "\\xa4" DQ); + test_quoting_of(is_tested, 0xa5, DQ "\\xa5" DQ); + test_quoting_of(is_tested, 0xa6, DQ "\\xa6" DQ); + test_quoting_of(is_tested, 0xa7, DQ "\\xa7" DQ); + test_quoting_of(is_tested, 0xa8, DQ "\\xa8" DQ); + test_quoting_of(is_tested, 0xa9, DQ "\\xa9" DQ); + test_quoting_of(is_tested, 0xaa, DQ "\\xaa" DQ); + test_quoting_of(is_tested, 0xab, DQ "\\xab" DQ); + test_quoting_of(is_tested, 0xac, DQ "\\xac" DQ); + test_quoting_of(is_tested, 0xad, DQ "\\xad" DQ); + test_quoting_of(is_tested, 0xae, DQ "\\xae" DQ); + test_quoting_of(is_tested, 0xaf, DQ "\\xaf" DQ); + // block 12 (8-bit): 0xb0 - 0xbf + test_quoting_of(is_tested, 0xb0, DQ "\\xb0" DQ); + test_quoting_of(is_tested, 0xb1, DQ "\\xb1" DQ); + test_quoting_of(is_tested, 0xb2, DQ "\\xb2" DQ); + test_quoting_of(is_tested, 0xb3, DQ "\\xb3" DQ); + test_quoting_of(is_tested, 0xb4, DQ "\\xb4" DQ); + test_quoting_of(is_tested, 0xb5, DQ "\\xb5" DQ); + test_quoting_of(is_tested, 0xb6, DQ "\\xb6" DQ); + test_quoting_of(is_tested, 0xb7, DQ "\\xb7" DQ); + test_quoting_of(is_tested, 0xb8, DQ "\\xb8" DQ); + test_quoting_of(is_tested, 0xb9, DQ "\\xb9" DQ); + test_quoting_of(is_tested, 0xba, DQ "\\xba" DQ); + test_quoting_of(is_tested, 0xbb, DQ "\\xbb" DQ); + test_quoting_of(is_tested, 0xbc, DQ "\\xbc" DQ); + test_quoting_of(is_tested, 0xbd, DQ "\\xbd" DQ); + test_quoting_of(is_tested, 0xbe, DQ "\\xbe" DQ); + test_quoting_of(is_tested, 0xbf, DQ "\\xbf" DQ); + // block 13 (8-bit): 0xc0 - 0xcf + test_quoting_of(is_tested, 0xc0, DQ "\\xc0" DQ); + test_quoting_of(is_tested, 0xc1, DQ "\\xc1" DQ); + test_quoting_of(is_tested, 0xc2, DQ "\\xc2" DQ); + test_quoting_of(is_tested, 0xc3, DQ "\\xc3" DQ); + test_quoting_of(is_tested, 0xc4, DQ "\\xc4" DQ); + test_quoting_of(is_tested, 0xc5, DQ "\\xc5" DQ); + test_quoting_of(is_tested, 0xc6, DQ "\\xc6" DQ); + test_quoting_of(is_tested, 0xc7, DQ "\\xc7" DQ); + test_quoting_of(is_tested, 0xc8, DQ "\\xc8" DQ); + test_quoting_of(is_tested, 0xc9, DQ "\\xc9" DQ); + test_quoting_of(is_tested, 0xca, DQ "\\xca" DQ); + test_quoting_of(is_tested, 0xcb, DQ "\\xcb" DQ); + test_quoting_of(is_tested, 0xcc, DQ "\\xcc" DQ); + test_quoting_of(is_tested, 0xcd, DQ "\\xcd" DQ); + test_quoting_of(is_tested, 0xce, DQ "\\xce" DQ); + test_quoting_of(is_tested, 0xcf, DQ "\\xcf" DQ); + // block 14 (8-bit): 0xd0 - 0xdf + test_quoting_of(is_tested, 0xd0, DQ "\\xd0" DQ); + test_quoting_of(is_tested, 0xd1, DQ "\\xd1" DQ); + test_quoting_of(is_tested, 0xd2, DQ "\\xd2" DQ); + test_quoting_of(is_tested, 0xd3, DQ "\\xd3" DQ); + test_quoting_of(is_tested, 0xd4, DQ "\\xd4" DQ); + test_quoting_of(is_tested, 0xd5, DQ "\\xd5" DQ); + test_quoting_of(is_tested, 0xd6, DQ "\\xd6" DQ); + test_quoting_of(is_tested, 0xd7, DQ "\\xd7" DQ); + test_quoting_of(is_tested, 0xd8, DQ "\\xd8" DQ); + test_quoting_of(is_tested, 0xd9, DQ "\\xd9" DQ); + test_quoting_of(is_tested, 0xda, DQ "\\xda" DQ); + test_quoting_of(is_tested, 0xdb, DQ "\\xdb" DQ); + test_quoting_of(is_tested, 0xdc, DQ "\\xdc" DQ); + test_quoting_of(is_tested, 0xdd, DQ "\\xdd" DQ); + test_quoting_of(is_tested, 0xde, DQ "\\xde" DQ); + test_quoting_of(is_tested, 0xdf, DQ "\\xdf" DQ); + // block 15 (8-bit): 0xe0 - 0xef + test_quoting_of(is_tested, 0xe0, DQ "\\xe0" DQ); + test_quoting_of(is_tested, 0xe1, DQ "\\xe1" DQ); + test_quoting_of(is_tested, 0xe2, DQ "\\xe2" DQ); + test_quoting_of(is_tested, 0xe3, DQ "\\xe3" DQ); + test_quoting_of(is_tested, 0xe4, DQ "\\xe4" DQ); + test_quoting_of(is_tested, 0xe5, DQ "\\xe5" DQ); + test_quoting_of(is_tested, 0xe6, DQ "\\xe6" DQ); + test_quoting_of(is_tested, 0xe7, DQ "\\xe7" DQ); + test_quoting_of(is_tested, 0xe8, DQ "\\xe8" DQ); + test_quoting_of(is_tested, 0xe9, DQ "\\xe9" DQ); + test_quoting_of(is_tested, 0xea, DQ "\\xea" DQ); + test_quoting_of(is_tested, 0xeb, DQ "\\xeb" DQ); + test_quoting_of(is_tested, 0xec, DQ "\\xec" DQ); + test_quoting_of(is_tested, 0xed, DQ "\\xed" DQ); + test_quoting_of(is_tested, 0xee, DQ "\\xee" DQ); + test_quoting_of(is_tested, 0xef, DQ "\\xef" DQ); + // block 16 (8-bit): 0xf0 - 0xff + test_quoting_of(is_tested, 0xf0, DQ "\\xf0" DQ); + test_quoting_of(is_tested, 0xf1, DQ "\\xf1" DQ); + test_quoting_of(is_tested, 0xf2, DQ "\\xf2" DQ); + test_quoting_of(is_tested, 0xf3, DQ "\\xf3" DQ); + test_quoting_of(is_tested, 0xf4, DQ "\\xf4" DQ); + test_quoting_of(is_tested, 0xf5, DQ "\\xf5" DQ); + test_quoting_of(is_tested, 0xf6, DQ "\\xf6" DQ); + test_quoting_of(is_tested, 0xf7, DQ "\\xf7" DQ); + test_quoting_of(is_tested, 0xf8, DQ "\\xf8" DQ); + test_quoting_of(is_tested, 0xf9, DQ "\\xf9" DQ); + test_quoting_of(is_tested, 0xfa, DQ "\\xfa" DQ); + test_quoting_of(is_tested, 0xfb, DQ "\\xfb" DQ); + test_quoting_of(is_tested, 0xfc, DQ "\\xfc" DQ); + test_quoting_of(is_tested, 0xfd, DQ "\\xfd" DQ); + test_quoting_of(is_tested, 0xfe, DQ "\\xfe" DQ); + test_quoting_of(is_tested, 0xff, DQ "\\xff" DQ); + + // Ensure the search was exhaustive. + for (int i = 0; i <= 0xff; ++i) { + g_assert_true(is_tested[i]); + } + + // Few extra tests (repeated) for specific things. + + // Smoke test + sc_string_quote(buf, sizeof buf, "hello 123"); + g_assert_cmpstr(buf, ==, DQ "hello 123" DQ); + + // Whitespace + sc_string_quote(buf, sizeof buf, "\n"); + g_assert_cmpstr(buf, ==, DQ "\\n" DQ); + sc_string_quote(buf, sizeof buf, "\r"); + g_assert_cmpstr(buf, ==, DQ "\\r" DQ); + sc_string_quote(buf, sizeof buf, "\t"); + g_assert_cmpstr(buf, ==, DQ "\\t" DQ); + sc_string_quote(buf, sizeof buf, "\v"); + g_assert_cmpstr(buf, ==, DQ "\\v" DQ); + + // Escape character itself + sc_string_quote(buf, sizeof buf, "\\"); + g_assert_cmpstr(buf, ==, DQ "\\\\" DQ); + + // Double quote character + sc_string_quote(buf, sizeof buf, "\""); + g_assert_cmpstr(buf, ==, DQ "\\\"" DQ); + +#undef DQ +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/string-utils/sc_streq", test_sc_streq); + g_test_add_func("/string-utils/sc_endswith", test_sc_endswith); + g_test_add_func("/string-utils/sc_must_snprintf", + test_sc_must_snprintf); + g_test_add_func("/string-utils/sc_must_snprintf/fail", + test_sc_must_snprintf__fail); + g_test_add_func("/string-utils/sc_string_append/normal", + test_sc_string_append); + g_test_add_func("/string-utils/sc_string_append/empty_to_full", + test_sc_string_append__empty_to_full); + g_test_add_func("/string-utils/sc_string_append/overflow", + test_sc_string_append__overflow); + g_test_add_func("/string-utils/sc_string_append/uninitialized_buf", + test_sc_string_append__uninitialized_buf); + g_test_add_func("/string-utils/sc_string_append/NULL_buf", + test_sc_string_append__NULL_buf); + g_test_add_func("/string-utils/sc_string_append/NULL_str", + test_sc_string_append__NULL_str); + g_test_add_func("/string-utils/sc_string_init/normal", + test_sc_string_init__normal); + g_test_add_func("/string-utils/sc_string_init/empty_buf", + test_sc_string_init__empty_buf); + g_test_add_func("/string-utils/sc_string_init/NULL_buf", + test_sc_string_init__NULL_buf); + g_test_add_func + ("/string-utils/sc_string_append_char__uninitialized_buf", + test_sc_string_append_char__uninitialized_buf); + g_test_add_func("/string-utils/sc_string_append_char__NULL_buf", + test_sc_string_append_char__NULL_buf); + g_test_add_func("/string-utils/sc_string_append_char__overflow", + test_sc_string_append_char__overflow); + g_test_add_func("/string-utils/sc_string_append_char__invalid_zero", + test_sc_string_append_char__invalid_zero); + g_test_add_func("/string-utils/sc_string_append_char__normal", + test_sc_string_append_char__normal); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__NULL_buf", + test_sc_string_append_char_pair__NULL_buf); + g_test_add_func("/string-utils/sc_string_append_char_pair__overflow", + test_sc_string_append_char_pair__overflow); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__invalid_zero_c1", + test_sc_string_append_char_pair__invalid_zero_c1); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__invalid_zero_c2", + test_sc_string_append_char_pair__invalid_zero_c2); + g_test_add_func("/string-utils/sc_string_append_char_pair__normal", + test_sc_string_append_char_pair__normal); + g_test_add_func("/string-utils/sc_string_quote__NULL_buf", + test_sc_string_quote_NULL_str); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__uninitialized_buf", + test_sc_string_append_char_pair__uninitialized_buf); + g_test_add_func("/string-utils/sc_string_quote", test_sc_string_quote); +} diff --git a/cmd/libsnap-confine-private/string-utils.c b/cmd/libsnap-confine-private/string-utils.c new file mode 100644 index 00000000..0c9c8ee4 --- /dev/null +++ b/cmd/libsnap-confine-private/string-utils.c @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "string-utils.h" + +#include +#include +#include +#include + +#include "utils.h" + +bool sc_streq(const char *a, const char *b) +{ + if (!a || !b) { + return false; + } + + size_t alen = strlen(a); + size_t blen = strlen(b); + + if (alen != blen) { + return false; + } + + return strncmp(a, b, alen) == 0; +} + +bool sc_endswith(const char *str, const char *suffix) +{ + if (!str || !suffix) { + return false; + } + + size_t xlen = strlen(suffix); + size_t slen = strlen(str); + + if (slen < xlen) { + return false; + } + + return strncmp(str - xlen + slen, suffix, xlen) == 0; +} + +int sc_must_snprintf(char *str, size_t size, const char *format, ...) +{ + int n; + + va_list va; + va_start(va, format); + n = vsnprintf(str, size, format, va); + va_end(va); + + if (n < 0 || (size_t) n >= size) + die("cannot format string: %s", str); + + return n; +} + +size_t sc_string_append(char *dst, size_t dst_size, const char *str) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append string: buffer is NULL"); + } + if (str == NULL) { + die("cannot append string: string is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append string: dst is unterminated"); + } + + size_t max_str_len = dst_size - dst_len; + size_t str_len = strnlen(str, max_str_len); + if (str_len == max_str_len) { + die("cannot append string: str is too long or unterminated"); + } + // Append the string + memcpy(dst + dst_len, str, str_len); + // Ensure we are terminated + dst[dst_len + str_len] = '\0'; + // return the new size + return strlen(dst); +} + +size_t sc_string_append_char(char *dst, size_t dst_size, char c) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append character: buffer is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append character: dst is unterminated"); + } + size_t max_str_len = dst_size - dst_len; + if (max_str_len < 2) { + die("cannot append character: not enough space"); + } + if (c == 0) { + die("cannot append character: cannot append string terminator"); + } + // Append the character and terminate the string. + dst[dst_len + 0] = c; + dst[dst_len + 1] = '\0'; + // Return the new size + return dst_len + 1; +} + +size_t sc_string_append_char_pair(char *dst, size_t dst_size, char c1, char c2) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append character pair: buffer is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append character pair: dst is unterminated"); + } + size_t max_str_len = dst_size - dst_len; + if (max_str_len < 3) { + die("cannot append character pair: not enough space"); + } + if (c1 == 0 || c2 == 0) { + die("cannot append character pair: cannot append string terminator"); + } + // Append the two characters and terminate the string. + dst[dst_len + 0] = c1; + dst[dst_len + 1] = c2; + dst[dst_len + 2] = '\0'; + // Return the new size + return dst_len + 2; +} + +void sc_string_init(char *buf, size_t buf_size) +{ + errno = 0; + if (buf == NULL) { + die("cannot initialize string, buffer is NULL"); + } + if (buf_size == 0) { + die("cannot initialize string, buffer is too small"); + } + buf[0] = '\0'; +} + +void sc_string_quote(char *buf, size_t buf_size, const char *str) +{ + if (str == NULL) { + die("cannot quote string: string is NULL"); + } + const char *hex = "0123456789abcdef"; + // NOTE: this also checks buf/buf_size sanity so that we don't have to. + sc_string_init(buf, buf_size); + sc_string_append_char(buf, buf_size, '"'); + for (unsigned char c; (c = *str) != 0; ++str) { + switch (c) { + // Pass ASCII letters and digits unmodified. + case '0' ... '9': + case 'A' ... 'Z': + case 'a' ... 'z': + // Pass most of the punctuation unmodified. + case ' ': + case '!': + case '#': + case '$': + case '%': + case '&': + case '(': + case ')': + case '*': + case '+': + case ',': + case '-': + case '.': + case '/': + case ':': + case ';': + case '<': + case '=': + case '>': + case '?': + case '@': + case '[': + case '\'': + case ']': + case '^': + case '_': + case '`': + case '{': + case '|': + case '}': + case '~': + sc_string_append_char(buf, buf_size, c); + break; + // Escape special whitespace characters. + case '\n': + sc_string_append_char_pair(buf, buf_size, '\\', 'n'); + break; + case '\r': + sc_string_append_char_pair(buf, buf_size, '\\', 'r'); + break; + case '\t': + sc_string_append_char_pair(buf, buf_size, '\\', 't'); + break; + case '\v': + sc_string_append_char_pair(buf, buf_size, '\\', 'v'); + break; + // Escape the escape character. + case '\\': + sc_string_append_char_pair(buf, buf_size, '\\', '\\'); + break; + // Escape double quote character. + case '"': + sc_string_append_char_pair(buf, buf_size, '\\', '"'); + break; + // Escape everything else as a generic hexadecimal escape string. + default: + sc_string_append_char_pair(buf, buf_size, '\\', 'x'); + sc_string_append_char_pair(buf, buf_size, hex[c >> 4], + hex[c & 15]); + break; + } + } + sc_string_append_char(buf, buf_size, '"'); +} diff --git a/cmd/libsnap-confine-private/string-utils.h b/cmd/libsnap-confine-private/string-utils.h new file mode 100644 index 00000000..7b55eb25 --- /dev/null +++ b/cmd/libsnap-confine-private/string-utils.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_STRING_UTILS_H +#define SNAP_CONFINE_STRING_UTILS_H + +#include +#include + +/** + * Check if two strings are equal. + **/ +bool sc_streq(const char *a, const char *b); + +/** + * Check if a string has a given suffix. + **/ +bool sc_endswith(const char *str, const char *suffix); + +/** + * Safer version of snprintf. + * + * This version dies on any error condition. + **/ +__attribute__ ((format(printf, 3, 4))) +int sc_must_snprintf(char *str, size_t size, const char *format, ...); + +/** + * Append a string to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * The buffers cannot overlap. + **/ +size_t sc_string_append(char *dst, size_t dst_size, const char *str); + +/** + * Append a single character to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * The character cannot be the string terminator. + * + * The return value is the new length of the string. + **/ +size_t sc_string_append_char(char *dst, size_t dst_size, char c); + +/** + * Append a pair of characters to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * Neither character can be the string terminator. + * + * The return value is the new length of the string. + **/ +size_t sc_string_append_char_pair(char *dst, size_t dst_size, char c1, char c2); + +/** + * Initialize a string (make it empty). + * + * Initialize a string as empty, ensuring buf is non-NULL buf_size is > 0. + **/ +void sc_string_init(char *buf, size_t buf_size); + +/** + * Quote a string so it is safe for printing. + * + * This function is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * The function "quotes" the content of the given string into the given buffer. + * The buffer must be of sufficient size. Apart from letters and digits and + * some punctuation all characters are escaped using their hexadecimal escape + * codes. + * + * As a practical consideration the buffer should be of the following capacity: + * strlen(str) * 4 + 2 + 1; This corresponds to the most pessimistic escape + * process (each character is escaped to a hexadecimal value like \x05, two + * double-quote characters (one front, one rear) and the final string + * terminator character. + **/ +void sc_string_quote(char *buf, size_t buf_size, const char *str); + +#endif diff --git a/cmd/libsnap-confine-private/test-utils-test.c b/cmd/libsnap-confine-private/test-utils-test.c new file mode 100644 index 00000000..04894c45 --- /dev/null +++ b/cmd/libsnap-confine-private/test-utils-test.c @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "test-utils.h" + +#include +#include + +#include + +// Check that rm_rf_tmp doesn't remove things outside of /tmp +static void test_rm_rf_tmp(void) +{ + if (access("/nonexistent", F_OK) == 0) { + g_test_message + ("/nonexistent exists but this test doesn't want it to"); + g_test_fail(); + return; + } + if (g_test_subprocess()) { + rm_rf_tmp("/nonexistent"); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/test-utils/rm_rf_tmp", test_rm_rf_tmp); +} diff --git a/cmd/libsnap-confine-private/test-utils.c b/cmd/libsnap-confine-private/test-utils.c new file mode 100644 index 00000000..ab13ce4b --- /dev/null +++ b/cmd/libsnap-confine-private/test-utils.c @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "test-utils.h" + +#include "error.h" +#include "utils.h" + +#include + +void rm_rf_tmp(const char *dir) +{ + // Sanity check, don't remove anything that's not in the temporary + // directory. This is here to prevent unintended data loss. + if (!g_str_has_prefix(dir, "/tmp/")) + die("refusing to remove: %s", dir); + const gchar *working_directory = NULL; + gchar **argv = NULL; + gchar **envp = NULL; + GSpawnFlags flags = G_SPAWN_SEARCH_PATH; + GSpawnChildSetupFunc child_setup = NULL; + gpointer user_data = NULL; + gchar **standard_output = NULL; + gchar **standard_error = NULL; + gint exit_status = 0; + GError *error = NULL; + + argv = calloc(5, sizeof *argv); + if (argv == NULL) + die("cannot allocate command argument array"); + argv[0] = g_strdup("rm"); + if (argv[0] == NULL) + die("cannot allocate memory"); + argv[1] = g_strdup("-rf"); + if (argv[1] == NULL) + die("cannot allocate memory"); + argv[2] = g_strdup("--"); + if (argv[2] == NULL) + die("cannot allocate memory"); + argv[3] = g_strdup(dir); + if (argv[3] == NULL) + die("cannot allocate memory"); + argv[4] = NULL; + g_assert_true(g_spawn_sync + (working_directory, argv, envp, flags, child_setup, + user_data, standard_output, standard_error, &exit_status, + &error)); + g_assert_true(g_spawn_check_exit_status(exit_status, NULL)); + if (error != NULL) { + g_test_message("cannot remove temporary directory: %s\n", + error->message); + g_error_free(error); + } + g_free(argv[0]); + g_free(argv[1]); + g_free(argv[2]); + g_free(argv[3]); + g_free(argv); +} diff --git a/cmd/libsnap-confine-private/test-utils.h b/cmd/libsnap-confine-private/test-utils.h new file mode 100644 index 00000000..e13ffcbf --- /dev/null +++ b/cmd/libsnap-confine-private/test-utils.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_TEST_UTILS_H +#define SNAP_CONFINE_TEST_UTILS_H + +/** + * Shell-out to "rm -rf -- $dir" as long as $dir is in /tmp. + */ +void rm_rf_tmp(const char *dir); + +#endif diff --git a/cmd/libsnap-confine-private/unit-tests-main.c b/cmd/libsnap-confine-private/unit-tests-main.c new file mode 100644 index 00000000..f2c0993d --- /dev/null +++ b/cmd/libsnap-confine-private/unit-tests-main.c @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "unit-tests.h" + +int main(int argc, char **argv) +{ + return sc_run_unit_tests(&argc, &argv); +} diff --git a/cmd/libsnap-confine-private/unit-tests.c b/cmd/libsnap-confine-private/unit-tests.c new file mode 100644 index 00000000..c3a822e2 --- /dev/null +++ b/cmd/libsnap-confine-private/unit-tests.c @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "unit-tests.h" +#include + +int sc_run_unit_tests(int *argc, char ***argv) +{ + g_test_init(argc, argv, NULL); + g_test_set_nonfatal_assertions(); + return g_test_run(); +} diff --git a/cmd/libsnap-confine-private/unit-tests.h b/cmd/libsnap-confine-private/unit-tests.h new file mode 100644 index 00000000..31414ad5 --- /dev/null +++ b/cmd/libsnap-confine-private/unit-tests.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_UNIT_TESTS_H +#define SNAP_CONFINE_UNIT_TESTS_H + +/** + * Run unit tests and exit. + * + * The function inspects and modifies command line arguments. + * Internally it is using glib-test functions. + */ +int sc_run_unit_tests(int *argc, char ***argv); + +#endif // SNAP_CONFINE_SANITY_H diff --git a/cmd/libsnap-confine-private/utils-test.c b/cmd/libsnap-confine-private/utils-test.c new file mode 100644 index 00000000..e8782606 --- /dev/null +++ b/cmd/libsnap-confine-private/utils-test.c @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "utils.h" +#include "utils.c" + +#include + +static void test_parse_bool(void) +{ + int err; + bool value; + + value = false; + err = parse_bool("yes", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + value = false; + err = parse_bool("1", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + value = true; + err = parse_bool("no", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + value = true; + err = parse_bool("0", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + value = true; + err = parse_bool("", &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + value = true; + err = parse_bool(NULL, &value, false); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + value = false; + err = parse_bool(NULL, &value, true); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + value = true; + err = parse_bool("flower", &value, false); + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(errno, ==, EINVAL); + g_assert_true(value); + + err = parse_bool("yes", NULL, false); + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(errno, ==, EFAULT); +} + +static void test_die(void) +{ + if (g_test_subprocess()) { + errno = 0; + die("death message"); + g_test_message("expected die not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("death message\n"); +} + +static void test_die_with_errno(void) +{ + if (g_test_subprocess()) { + errno = EPERM; + die("death message"); + g_test_message("expected die not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("death message: Operation not permitted\n"); +} + +/** + * Perform the rest of testing in a ephemeral directory. + * + * Create a temporary directory, move the current process there and undo those + * operations at the end of the test. If any additional directories or files + * are created in this directory they must be removed by the caller. + **/ +static void g_test_in_ephemeral_dir(void) +{ + gchar *temp_dir = g_dir_make_tmp(NULL, NULL); + gchar *orig_dir = g_get_current_dir(); + int err = chdir(temp_dir); + g_assert_cmpint(err, ==, 0); + + g_test_queue_free(temp_dir); + g_test_queue_destroy((GDestroyNotify) rmdir, temp_dir); + g_test_queue_free(orig_dir); + g_test_queue_destroy((GDestroyNotify) chdir, orig_dir); +} + +/** + * Test sc_nonfatal_mkpath() given two directories. + **/ +static void _test_sc_nonfatal_mkpath(const gchar * dirname, + const gchar * subdirname) +{ + // Check that directory does not exist. + g_assert_false(g_file_test(dirname, G_FILE_TEST_EXISTS | + G_FILE_TEST_IS_DIR)); + // Use sc_nonfatal_mkpath to create the directory and ensure that it worked + // as expected. + g_test_queue_destroy((GDestroyNotify) rmdir, (char *)dirname); + int err = sc_nonfatal_mkpath(dirname, 0755); + g_assert_cmpint(err, ==, 0); + g_assert_cmpint(errno, ==, 0); + g_assert_true(g_file_test(dirname, G_FILE_TEST_EXISTS | + G_FILE_TEST_IS_REGULAR)); + // Use same function again to try to create the same directory and ensure + // that it didn't fail and properly retained EEXIST in errno. + err = sc_nonfatal_mkpath(dirname, 0755); + g_assert_cmpint(err, ==, 0); + g_assert_cmpint(errno, ==, EEXIST); + // Now create a sub-directory of the original directory and observe the + // results. We should no longer see errno of EEXIST! + g_test_queue_destroy((GDestroyNotify) rmdir, (char *)subdirname); + err = sc_nonfatal_mkpath(subdirname, 0755); + g_assert_cmpint(err, ==, 0); + g_assert_cmpint(errno, ==, 0); +} + +/** + * Test that sc_nonfatal_mkpath behaves when using relative paths. + **/ +static void test_sc_nonfatal_mkpath__relative(void) +{ + g_test_in_ephemeral_dir(); + gchar *current_dir = g_get_current_dir(); + g_test_queue_free(current_dir); + gchar *dirname = g_build_path("/", current_dir, "foo", NULL); + g_test_queue_free(dirname); + gchar *subdirname = g_build_path("/", current_dir, "foo", "bar", NULL); + g_test_queue_free(subdirname); + _test_sc_nonfatal_mkpath(dirname, subdirname); +} + +/** + * Test that sc_nonfatal_mkpath behaves when using absolute paths. + **/ +static void test_sc_nonfatal_mkpath__absolute(void) +{ + g_test_in_ephemeral_dir(); + const char *dirname = "foo"; + const char *subdirname = "foo/bar"; + _test_sc_nonfatal_mkpath(dirname, subdirname); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/utils/parse_bool", test_parse_bool); + g_test_add_func("/utils/die", test_die); + g_test_add_func("/utils/die_with_errno", test_die_with_errno); + g_test_add_func("/utils/sc_nonfatal_mkpath/relative", + test_sc_nonfatal_mkpath__relative); + g_test_add_func("/utils/sc_nonfatal_mkpath/absolute", + test_sc_nonfatal_mkpath__absolute); +} diff --git a/cmd/libsnap-confine-private/utils.c b/cmd/libsnap-confine-private/utils.c new file mode 100644 index 00000000..c0170eae --- /dev/null +++ b/cmd/libsnap-confine-private/utils.c @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" +#include "cleanup-funcs.h" + +void die(const char *msg, ...) +{ + int saved_errno = errno; + va_list va; + va_start(va, msg); + vfprintf(stderr, msg, va); + va_end(va); + + if (errno != 0) { + fprintf(stderr, ": %s\n", strerror(saved_errno)); + } else { + fprintf(stderr, "\n"); + } + exit(1); +} + +bool error(const char *msg, ...) +{ + va_list va; + va_start(va, msg); + vfprintf(stderr, msg, va); + va_end(va); + + return false; +} + +struct sc_bool_name { + const char *text; + bool value; +}; + +static const struct sc_bool_name sc_bool_names[] = { + {"yes", true}, + {"no", false}, + {"1", true}, + {"0", false}, + {"", false}, +}; + +/** + * Convert string to a boolean value, with a default. + * + * The return value is 0 in case of success or -1 when the string cannot be + * converted correctly. In such case errno is set to indicate the problem and + * the value is not written back to the caller-supplied pointer. + * + * If the text cannot be recognized, the default value is used. + **/ +static int parse_bool(const char *text, bool * value, bool default_value) +{ + if (value == NULL) { + errno = EFAULT; + return -1; + } + if (text == NULL) { + *value = default_value; + return 0; + } + for (size_t i = 0; i < sizeof sc_bool_names / sizeof *sc_bool_names; + ++i) { + if (strcmp(text, sc_bool_names[i].text) == 0) { + *value = sc_bool_names[i].value; + return 0; + } + } + errno = EINVAL; + return -1; +} + +/** + * Get an environment variable and convert it to a boolean. + * + * Supported values are those of parse_bool(), namely "yes", "no" as well as "1" + * and "0". All other values are treated as false and a diagnostic message is + * printed to stderr. If the environment variable is unset, set value to the + * default_value as if the environment variable was set to default_value. + **/ +static bool getenv_bool(const char *name, bool default_value) +{ + const char *str_value = getenv(name); + bool value = default_value; + if (parse_bool(str_value, &value, default_value) < 0) { + if (errno == EINVAL) { + fprintf(stderr, + "WARNING: unrecognized value of environment variable %s (expected yes/no or 1/0)\n", + name); + return false; + } else { + die("cannot convert value of environment variable %s to a boolean", name); + } + } + return value; +} + +bool sc_is_debug_enabled(void) +{ + return getenv_bool("SNAP_CONFINE_DEBUG", false) + || getenv_bool("SNAPD_DEBUG", false); +} + +bool sc_is_reexec_enabled(void) +{ + return getenv_bool("SNAP_REEXEC", true); +} + +void debug(const char *msg, ...) +{ + if (sc_is_debug_enabled()) { + va_list va; + va_start(va, msg); + fprintf(stderr, "DEBUG: "); + vfprintf(stderr, msg, va); + fprintf(stderr, "\n"); + va_end(va); + } +} + +void write_string_to_file(const char *filepath, const char *buf) +{ + debug("write_string_to_file %s %s", filepath, buf); + FILE *f = fopen(filepath, "w"); + if (f == NULL) + die("fopen %s failed", filepath); + if (fwrite(buf, strlen(buf), 1, f) != 1) + die("fwrite failed"); + if (fflush(f) != 0) + die("fflush failed"); + if (fclose(f) != 0) + die("fclose failed"); +} + +int sc_nonfatal_mkpath(const char *const path, mode_t mode) +{ + // If asked to create an empty path, return immediately. + if (strlen(path) == 0) { + return 0; + } + // We're going to use strtok_r, which needs to modify the path, so we'll + // make a copy of it. + char *path_copy SC_CLEANUP(sc_cleanup_string) = NULL; + path_copy = strdup(path); + if (path_copy == NULL) { + return -1; + } + // Open flags to use while we walk the user data path: + // - Don't follow symlinks + // - Don't allow child access to file descriptor + // - Only open a directory (fail otherwise) + const int open_flags = O_NOFOLLOW | O_CLOEXEC | O_DIRECTORY; + + // We're going to create each path segment via openat/mkdirat calls instead + // of mkdir calls, to avoid following symlinks and placing the user data + // directory somewhere we never intended for it to go. The first step is to + // get an initial file descriptor. + int fd SC_CLEANUP(sc_cleanup_close) = AT_FDCWD; + if (path_copy[0] == '/') { + fd = open("/", open_flags); + if (fd < 0) { + return -1; + } + } + // strtok_r needs a pointer to keep track of where it is in the string. + char *path_walker = NULL; + + // Initialize tokenizer and obtain first path segment. + char *path_segment = strtok_r(path_copy, "/", &path_walker); + while (path_segment) { + // Try to create the directory. It's okay if it already existed, but + // return with error on any other error. Reset errno before attempting + // this as it may stay stale (errno is not reset if mkdirat(2) returns + // successfully). + errno = 0; + if (mkdirat(fd, path_segment, mode) < 0 && errno != EEXIST) { + return -1; + } + // Open the parent directory we just made (and close the previous one + // (but not the special value AT_FDCWD) so we can continue down the + // path. + int previous_fd = fd; + fd = openat(fd, path_segment, open_flags); + if (previous_fd != AT_FDCWD && close(previous_fd) != 0) { + return -1; + } + if (fd < 0) { + return -1; + } + // Obtain the next path segment. + path_segment = strtok_r(NULL, "/", &path_walker); + } + return 0; +} diff --git a/cmd/libsnap-confine-private/utils.h b/cmd/libsnap-confine-private/utils.h new file mode 100644 index 00000000..b544f6cf --- /dev/null +++ b/cmd/libsnap-confine-private/utils.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#ifndef CORE_LAUNCHER_UTILS_H +#define CORE_LAUNCHER_UTILS_H + +#include +#include + +__attribute__ ((noreturn)) + __attribute__ ((format(printf, 1, 2))) +void die(const char *fmt, ...); + +__attribute__ ((format(printf, 1, 2))) +bool error(const char *fmt, ...); + +__attribute__ ((format(printf, 1, 2))) +void debug(const char *fmt, ...); + +/** + * Return true if debugging is enabled. + * + * This can used to avoid costly computation that is only useful for debugging. + **/ +bool sc_is_debug_enabled(void); + +/** + * Return true if re-execution is enabled. + **/ +bool sc_is_reexec_enabled(void); + +void write_string_to_file(const char *filepath, const char *buf); + +/** + * Safely create a given directory. + * + * NOTE: non-fatal functions don't die on errors. It is the responsibility of + * the caller to call die() or handle the error appropriately. + * + * This function behaves like "mkdir -p" (recursive mkdir) with the exception + * that each directory is carefully created in a way that avoids symlink + * attacks. The preceding directory is kept openat(2) (along with O_DIRECTORY) + * and the next directory is created using mkdirat(2), this sequence continues + * while there are more directories to process. + * + * The function returns -1 in case of any error. + **/ +__attribute__ ((warn_unused_result)) +int sc_nonfatal_mkpath(const char *const path, mode_t mode); +#endif diff --git a/cmd/snap-confine/PORTING b/cmd/snap-confine/PORTING new file mode 100644 index 00000000..43869489 --- /dev/null +++ b/cmd/snap-confine/PORTING @@ -0,0 +1,15 @@ +Welcome brave porters! + +This file is intended to guide you towards porting snappy (comprised of snapd +and this project, snap-confine) to work on a new kernel. The confinement setup by +snap-confine has several requirements on the kernel. + +TODO: list required patches (apparmor, seccomp) +TODO: list required kernel configufation +TODO: list minimum supported kernel version + +While you are working on porting those patches to your kernel of choice, you +may configure snap-confine with --disable-security. This switch drops +requirement on apparmor, seccomp and udev and reduces snap-confine to arrange +the filesystem in a correct way for snaps to operate without really confining +them in any way. diff --git a/cmd/snap-confine/README.mount_namespace b/cmd/snap-confine/README.mount_namespace new file mode 100644 index 00000000..a44e145d --- /dev/null +++ b/cmd/snap-confine/README.mount_namespace @@ -0,0 +1,138 @@ += Mount namespace setup in snap-confine = + +This document provides a terse explanation of the mount setup using syscall +traces to show precisely what is happening and show the difference between +all snaps images and classic. + +Obtain traces with (ignoring select helps keep strace from hanging): +$ sudo snap install hello-world +$ sudo /usr/lib/snapd/snap-discard-ns hello-world +$ sudo strace -f -vv -s8192 -o /tmp/trace.unshare -e trace='!select' /snap/bin/hello-world +$ sudo strace -f -vv -s8192 -o /tmp/trace.setns -e trace='!select' /snap/bin/hello-world + +Examine /tmp/trace.unshare for initial mount namespace setup and +/tmp/trace.setns for seeing how the mount namespace is reused on subsequent +runs. Note that running /usr/lib/snapd/snap-discard-ns prior to running the +command is required for creating the new mount namespace (otherwise the +previous mount namespace will be reused). + + += Mount namespace setup in detail = +Here are the steps snap-confine takes when setting up the mount namespace for a +given snap: + +# Create the /run/snapd/ns directory to save off the mount namespace to be +# shared on other app-invocations +open("/", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 3 +mkdirat(3, "run", 0755) = -1 EEXIST (File exists) +openat(3, "run", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 4 +mkdirat(4, "snapd", 0755) = -1 EEXIST (File exists) +openat(4, "snapd", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 3 +mkdirat(3, "ns", 0755) = -1 EEXIST (File exists) +openat(3, "ns", O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC) = 4 + +# If /run/snapd/ns/.mnt exists, enter that namespace: +openat(3, "hello-world.mnt", O_RDONLY|O_CREAT|O_NOFOLLOW|O_CLOEXEC, 0600) = 5 +fstatfs(5, {f_type=0x6e736673, ...) = 0 +setns(5, CLONE_NEWNS) = 0 +... mount namespace setup finished, go on to setup the rest of the sandbox ... + + +# Otherwise, create a new mount namespace +unshare(CLONE_NEWNS) +mount("none", "/", NULL, MS_REC|MS_SLAVE, NULL) = 0 + +# Classic-only - mount rootfs in the namespace +mkdir("/tmp/snap.rootfs_HkQghZ", 0700) = 0 +mount("/snap/ubuntu-core/current", "/tmp/snap.rootfs_HkQghZ", NULL, MS_BIND, NULL) = 0 + +# Classic only - mount directories from host over rootfs +mount("/dev", "/tmp/snap.rootfs_HkQghZ/dev", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/etc", "/tmp/snap.rootfs_HkQghZ/etc", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/home", "/tmp/snap.rootfs_HkQghZ/home", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/root", "/tmp/snap.rootfs_HkQghZ/root", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/proc", "/tmp/snap.rootfs_HkQghZ/proc", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/sys", "/tmp/snap.rootfs_HkQghZ/sys", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/tmp", "/tmp/snap.rootfs_HkQghZ/tmp", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/var/snap", "/tmp/snap.rootfs_HkQghZ/var/snap", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/var/lib/snapd", "/tmp/snap.rootfs_HkQghZ/var/lib/snapd", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/var/tmp", "/tmp/snap.rootfs_HkQghZ/var/tmp", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/run", "/tmp/snap.rootfs_HkQghZ/run", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/media", "/tmp/snap.rootfs_HkQghZ/media", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/lib/modules", "/tmp/snap.rootfs_HkQghZ/lib/modules", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/usr/src", "/tmp/snap.rootfs_HkQghZ/usr/src", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/var/log", "/tmp/snap.rootfs_HkQghZ/var/log", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/snap", "/tmp/snap.rootfs_HkQghZ/snap", NULL, MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/snap/ubuntu-core/current/etc/alternatives", "/tmp/snap.rootfs_HkQghZ/etc/alternatives", NULL, MS_BIND|MS_SLAVE, NULL) = 0 +mount("/", "/tmp/snap.rootfs_HkQghZ/var/lib/snapd/hostfs", NULL, MS_RDONLY|MS_BIND, NULL) = 0 + +# Classic only - pivot_root into the rootfs +pivot_root(".", ".") = 0 +umount2(".", MNT_DETACH) = 0 + +# Create a bind-mounted private /tmp +mkdir("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1", 0700) = 0 +mkdir("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1/tmp", 01777) = 0 +mount("/tmp/snap.0_snap.hello-world.hello-world_QXGSt1/tmp", "/tmp", NULL, MS_BIND, NULL) = 0 +mount("none", "/tmp", NULL, MS_PRIVATE, NULL) = 0 + +# Create a per-snap /dev/pts +mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL, "newinstance,ptmxmode=0666,mode=0"...) +mount("/dev/pts/ptmx", "/dev/ptmx", 0x5574dfe9a5c3, MS_BIND, NULL) + +# Classic only - process quirks mounts by: +# creating temporary quirks directory for moving /var/lib/snapd aside +mkdir("/tmp/snapd.quirks_xKIzG3", 0700) = 0 +# moving /var/lib/snapd aside +mount("/var/lib/snapd", "/tmp/snapd.quirks_xKIzG3", NULL, MS_MOVE, NULL) = 0 +# creating a tmpfs on /var/lib for our mount points +mount("none", "/var/lib", "tmpfs", MS_NOSUID|MS_NODEV, NULL) = 0 +# mimicking the vanilla /var/lib/* from the core snap in /var/lib in tmpfs +# (the directories to mimic are dynamically determined and will vary as the +# core snap changes. Syscalls for finding what to mount and creating the +# mount points are omitted) +mount("/snap/ubuntu-core/current/var/lib/apparmor", "/var/lib/apparmor", NULL, MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 +mount("/snap/ubuntu-core/current/var/lib/classic", "/var/lib/classic", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/console-conf", "/var/lib/console-conf", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/dbus", "/var/lib/dbus", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/dhcp", "/var/lib/dhcp", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/extrausers", "/var/lib/extrausers", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/initramfs-tools", "/var/lib/initramfs-tools", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/initscripts", "/var/lib/initscripts", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/insserv", "/var/lib/insserv", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/logrotate", "/var/lib/logrotate", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/machines", "/var/lib/machines", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/misc", "/var/lib/misc", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/pam", "/var/lib/pam", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/python", "/var/lib/python", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/resolvconf", "/var/lib/resolvconf", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/snapd", "/var/lib/snapd", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/sudo", "/var/lib/sudo", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/systemd", "/var/lib/systemd", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/ubuntu-fan", "/var/lib/ubuntu-fan", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/ucf", "/var/lib/ucf", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/update-rc.d", "/var/lib/update-rc.d", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/urandom", "/var/lib/urandom", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/vim", "/var/lib/vim", ...) = 0 +mount("/snap/ubuntu-core/current/var/lib/waagent", "/var/lib/waagent", ...) = 0 +# unmounting the /var/lib/snapd that was just mimicked +umount2("/var/lib/snapd", 0) +# moving back the /var/lib/snapd that was set aside +mount("/tmp/snapd.quirks_xKIzG3", "/var/lib/snapd", NULL, MS_MOVE, NULL) = 0 +# cleaning up the temporary directory +rmdir("/tmp/snapd.quirks_xKIzG3") = 0 +# applying the actual quirk mounts as needed (for now, lxd, but more may +# come). Eg: +mount("/var/lib/snapd/hostfs/var/lib/lxd", "/var/lib/lxd", NULL, MS_REC|MS_SLAVE|MS_NODEV|MS_NOSUID|MS_NOEXEC) = 0 +# End quirk mounts on classic + +# Process snap-defined mounts (eg, for content interface, mount the source to +# the target as defined in /var/lib/snapd/mount/snap...fstab) +# Eg: +mount("/snap/some-content-snap/current/src", "/snap/hello-world/current/dst", NULL, MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND, NULL) + +# Bind mount this namespace to the application-specific NSFS magic file to +# preserve it across snap invocations (an fchdir() happened just after the +# unshare(), above). +mount("/proc/12887/ns/mnt", "hello-world.mnt", NULL, MS_BIND, NULL) = 0 +... mount namespace setup finished, go on to setup the rest of the sandbox ... diff --git a/cmd/snap-confine/README.nvidia b/cmd/snap-confine/README.nvidia new file mode 100644 index 00000000..bdcae4c2 --- /dev/null +++ b/cmd/snap-confine/README.nvidia @@ -0,0 +1,26 @@ +Nvidia on Arch +============== + +On Arch nvidia support differs depending on the version of the driver user. +Free drivers should work out of the box without any changes. Proprietary +drivers were tested on the following driver versions: + +nvidia-340xx 340.96-13 +nvidia-340xx-libgl 340.96-1 +nvidia-340xx-utils 340.96-1 + +The way the driver stack works was changed significantly in driver 364 and that +version does not yet work correctly (we will gladly take patches if you beat us +to the punch!). There is some ongoing work but it needs more investigation. + +Nvidia on Ubuntu +================ + +On Ubuntu nvidia drivers are provided in a different way and we believe that +all versions work correctly. + +Nvidia on $DISTRO +================= + +Free drivers should work everywhere. Support for proprietary drivers will be +added on a case-by-case basis. diff --git a/cmd/snap-confine/README.syscalls b/cmd/snap-confine/README.syscalls new file mode 100644 index 00000000..fee72df1 --- /dev/null +++ b/cmd/snap-confine/README.syscalls @@ -0,0 +1,436 @@ +To get all the syscalls, grab all the linux-libc-dev packages for all the +architectures (eg, amd64, arm64, armhf, i386, powerpc, ppc64el) and put then +in a directory. Then: + +mkdir extracted +for i in ./*deb ; do + dpkg-deb -x $i ./extracted +done + +for i in `find . -name "unistd*.h"|grep gnu` ; do egrep '^#define .*_NR_([a-z0-9_\-]*)' $i | awk '{print $2}' | sed 's/.*_NR_//' ; done|sort -u + +NOTE: syscall() isn't actually a syscall, it is a glibc wrapping to reference +a syscall by number (therefore, it should be omitted from filter policy). ARM +OABI did define this, but it has been obsoleted in EABI. + +For example, on Ubuntu 16.04 with the 4.4.0-16.32 Linux kernel, these are the +syscalls: +accept +accept4 +access +acct +add_key +adjtimex +afs_syscall +alarm +arch_prctl +arm_fadvise64_64 +arm_sync_file_range +bdflush +bind +bpf +break +breakpoint +brk +cacheflush +capget +capset +chdir +chmod +chown +chown32 +chroot +clock_adjtime +clock_getres +clock_gettime +clock_nanosleep +clock_settime +clone +close +connect +creat +create_module +delete_module +dup +dup2 +dup3 +epoll_create +epoll_create1 +epoll_ctl +epoll_ctl_old +epoll_pwait +epoll_wait +epoll_wait_old +eventfd +eventfd2 +execve +execveat +exit +exit_group +faccessat +fadvise64 +fadvise64_64 +fallocate +fanotify_init +fanotify_mark +fchdir +fchmod +fchmodat +fchown +fchown32 +fchownat +fcntl +fcntl64 +fdatasync +fgetxattr +finit_module +flistxattr +flock +fork +fremovexattr +fsetxattr +fstat +fstat64 +fstatat64 +fstatfs +fstatfs64 +fsync +ftime +ftruncate +ftruncate64 +futex +futimesat +getcpu +getcwd +getdents +getdents64 +getegid +getegid32 +geteuid +geteuid32 +getgid +getgid32 +getgroups +getgroups32 +getitimer +get_kernel_syms +get_mempolicy +getpeername +getpgid +getpgrp +getpid +getpmsg +getppid +getpriority +getrandom +getresgid +getresgid32 +getresuid +getresuid32 +getrlimit +get_robust_list +getrusage +getsid +getsockname +getsockopt +get_thread_area +gettid +gettimeofday +getuid +getuid32 +getxattr +gtty +idle +init_module +inotify_add_watch +inotify_init +inotify_init1 +inotify_rm_watch +io_cancel +ioctl +io_destroy +io_getevents +ioperm +iopl +ioprio_get +ioprio_set +io_setup +io_submit +ipc +kcmp +kexec_file_load +kexec_load +keyctl +kill +lchown +lchown32 +lgetxattr +link +linkat +listen +listxattr +llistxattr +_llseek +lock +lookup_dcookie +lremovexattr +lseek +lsetxattr +lstat +lstat64 +madvise +mbind +membarrier +memfd_create +migrate_pages +mincore +mkdir +mkdirat +mknod +mknodat +mlock +mlock2 +mlockall +mmap +mmap2 +modify_ldt +mount +move_pages +mprotect +mpx +mq_getsetattr +mq_notify +mq_open +mq_timedreceive +mq_timedsend +mq_unlink +mremap +msgctl +msgget +msgrcv +msgsnd +msync +multiplexer +munlock +munlockall +munmap +name_to_handle_at +nanosleep +newfstatat +_newselect +nfsservctl +nice +oldfstat +oldlstat +oldolduname +oldstat +olduname +open +openat +open_by_handle_at +pause +pciconfig_iobase +pciconfig_read +pciconfig_write +perf_event_open +personality +pipe +pipe2 +pivot_root +poll +ppoll +prctl +pread64 +preadv +prlimit64 +process_vm_readv +process_vm_writev +prof +profil +pselect6 +ptrace +putpmsg +pwrite64 +pwritev +query_module +quotactl +read +readahead +readdir +readlink +readlinkat +readv +reboot +recv +recvfrom +recvmmsg +recvmsg +remap_file_pages +removexattr +rename +renameat +renameat2 +request_key +restart_syscall +rmdir +rtas +rt_sigaction +rt_sigpending +rt_sigprocmask +rt_sigqueueinfo +rt_sigreturn +rt_sigsuspend +rt_sigtimedwait +rt_tgsigqueueinfo +s390_pci_mmio_read +s390_pci_mmio_write +s390_runtime_instr +sched_getaffinity +sched_getattr +sched_getparam +sched_get_priority_max +sched_get_priority_min +sched_getscheduler +sched_rr_get_interval +sched_setaffinity +sched_setattr +sched_setparam +sched_setscheduler +sched_yield +seccomp +security +select +semctl +semget +semop +semtimedop +send +sendfile +sendfile64 +sendmmsg +sendmsg +sendto +setdomainname +setfsgid +setfsgid32 +setfsuid +setfsuid32 +setgid +setgid32 +setgroups +setgroups32 +sethostname +setitimer +set_mempolicy +setns +setpgid +setpriority +setregid +setregid32 +setresgid +setresgid32 +setresuid +setresuid32 +setreuid +setreuid32 +setrlimit +set_robust_list +setsid +setsockopt +set_thread_area +set_tid_address +settimeofday +set_tls +setuid +setuid32 +setxattr +sgetmask +shmat +shmctl +shmdt +shmget +shutdown +sigaction +sigaltstack +signal +signalfd +signalfd4 +sigpending +sigprocmask +sigreturn +sigsuspend +socket +socketcall +socketpair +splice +spu_create +spu_run +ssetmask +stat +stat64 +statfs +statfs64 +stime +stty +subpage_prot +swapcontext +swapoff +swapon +switch_endian +symlink +symlinkat +sync +sync_file_range +sync_file_range2 +syncfs +syscall +_sysctl +sys_debug_setcontext +sysfs +sysinfo +syslog +tee +tgkill +time +timer_create +timer_delete +timerfd +timerfd_create +timerfd_gettime +timerfd_settime +timer_getoverrun +timer_gettime +timer_settime +times +tkill +truncate +truncate64 +tuxcall +ugetrlimit +ulimit +umask +umount +umount2 +uname +unlink +unlinkat +unshare +uselib +userfaultfd +usr26 +usr32 +ustat +utime +utimensat +utimes +vfork +vhangup +vm86 +vm86old +vmsplice +vserver +wait4 +waitid +waitpid +write +writev diff --git a/cmd/snap-confine/apparmor-support.c b/cmd/snap-confine/apparmor-support.c new file mode 100644 index 00000000..eac0912d --- /dev/null +++ b/cmd/snap-confine/apparmor-support.c @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "apparmor-support.h" + +#include +#include +#ifdef HAVE_APPARMOR +#include +#endif // ifdef HAVE_APPARMOR + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/utils.h" + +// NOTE: Those constants map exactly what apparmor is returning and cannot be +// changed without breaking apparmor functionality. +#define SC_AA_ENFORCE_STR "enforce" +#define SC_AA_COMPLAIN_STR "complain" +#define SC_AA_MIXED_STR "mixed" +#define SC_AA_UNCONFINED_STR "unconfined" + +void sc_init_apparmor_support(struct sc_apparmor *apparmor) +{ +#ifdef HAVE_APPARMOR + // Use aa_is_enabled() to see if apparmor is available in the kernel and + // enabled at boot time. If it isn't log a diagnostic message and assume + // we're not confined. + if (aa_is_enabled() != true) { + switch (errno) { + case ENOSYS: + debug + ("apparmor extensions to the system are not available"); + break; + case ECANCELED: + debug + ("apparmor is available on the system but has been disabled at boot"); + break; + case ENOENT: + debug + ("apparmor is available but the interface but the interface is not available"); + break; + case EPERM: + // NOTE: fall-through + case EACCES: + debug + ("insufficient permissions to determine if apparmor is enabled"); + break; + default: + debug("apparmor is not enabled: %s", strerror(errno)); + break; + } + apparmor->is_confined = false; + apparmor->mode = SC_AA_NOT_APPLICABLE; + return; + } + // Use aa_getcon() to check the label of the current process and + // confinement type. Note that the returned label must be released with + // free() but the mode is a constant string that must not be freed. + char *label SC_CLEANUP(sc_cleanup_string) = NULL; + char *mode = NULL; + if (aa_getcon(&label, &mode) < 0) { + die("cannot query current apparmor profile"); + } + debug("apparmor label on snap-confine is: %s", label); + debug("apparmor mode is: %s", mode); + // The label has a special value "unconfined" that is applied to all + // processes without a dedicated profile. If that label is used then the + // current process is not confined. All other labels imply confinement. + if (label != NULL && strcmp(label, SC_AA_UNCONFINED_STR) == 0) { + apparmor->is_confined = false; + } else { + apparmor->is_confined = true; + } + // There are several possible results for the confinement type (mode) that + // are checked for below. + if (mode != NULL && strcmp(mode, SC_AA_COMPLAIN_STR) == 0) { + apparmor->mode = SC_AA_COMPLAIN; + } else if (mode != NULL && strcmp(mode, SC_AA_ENFORCE_STR) == 0) { + apparmor->mode = SC_AA_ENFORCE; + } else if (mode != NULL && strcmp(mode, SC_AA_MIXED_STR) == 0) { + apparmor->mode = SC_AA_MIXED; + } else { + apparmor->mode = SC_AA_INVALID; + } +#else + apparmor->mode = SC_AA_NOT_APPLICABLE; + apparmor->is_confined = false; +#endif // ifdef HAVE_APPARMOR +} + +void +sc_maybe_aa_change_onexec(struct sc_apparmor *apparmor, const char *profile) +{ +#ifdef HAVE_APPARMOR + if (apparmor->mode == SC_AA_NOT_APPLICABLE) { + return; + } + debug("requesting changing of apparmor profile on next exec to %s", + profile); + if (aa_change_onexec(profile) < 0) { + if (secure_getenv("SNAPPY_LAUNCHER_INSIDE_TESTS") == NULL) { + die("cannot change profile for the next exec call"); + } + } +#endif // ifdef HAVE_APPARMOR +} + +void +sc_maybe_aa_change_hat(struct sc_apparmor *apparmor, + const char *subprofile, unsigned long magic_token) +{ +#ifdef HAVE_APPARMOR + if (apparmor->mode == SC_AA_NOT_APPLICABLE) { + return; + } + if (apparmor->is_confined) { + debug("changing apparmor hat to %s", subprofile); + if (aa_change_hat(subprofile, magic_token) < 0) { + die("cannot change apparmor hat"); + } + } +#endif // ifdef HAVE_APPARMOR +} diff --git a/cmd/snap-confine/apparmor-support.h b/cmd/snap-confine/apparmor-support.h new file mode 100644 index 00000000..b90f285c --- /dev/null +++ b/cmd/snap-confine/apparmor-support.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_APPARMOR_SUPPORT_H +#define SNAP_CONFINE_APPARMOR_SUPPORT_H + +#include + +/** + * Type of apparmor confinement. + **/ +enum sc_apparmor_mode { + // The enforcement mode was not recognized. + SC_AA_INVALID = -1, + // The enforcement mode is not applicable because apparmor is disabled. + SC_AA_NOT_APPLICABLE = 0, + // The enforcement mode is "enforcing" + SC_AA_ENFORCE = 1, + // The enforcement mode is "complain" + SC_AA_COMPLAIN, + // The enforcement mode is "mixed" + SC_AA_MIXED, +}; + +/** + * Data required to manage apparmor wrapper. + **/ +struct sc_apparmor { + // The mode of enforcement. In addition to the two apparmor defined modes + // can be also SC_AA_INVALID (unknown mode reported by apparmor) and + // SC_AA_NOT_APPLICABLE (when we're not linked with apparmor). + enum sc_apparmor_mode mode; + // Flag indicating that the current process is confined. + bool is_confined; +}; + +/** + * Initialize apparmor support. + * + * This operation should be done even when apparmor support is disabled at + * compile time. Internally the supplied structure is initialized based on the + * information returned from aa_getcon(2) or if apparmor is disabled at compile + * time, with built-in constants. + * + * The main action performed here is to check if snap-confine is currently + * confined, this information is used later in sc_maybe_change_apparmor_hat() + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. + **/ +void sc_init_apparmor_support(struct sc_apparmor *apparmor); + +/** + * Maybe call aa_change_onexec(2) + * + * This function does nothing when apparmor support is not enabled at compile + * time. If apparmor is enabled then profile change request is attempted. + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. As an exception, when SNAPPY_LAUNCHER_INSIDE_TESTS + * environment variable is set then the process is not terminated. + **/ +void +sc_maybe_aa_change_onexec(struct sc_apparmor *apparmor, const char *profile); + +/** + * Maybe call aa_change_hat(2) + * + * This function does nothing when apparmor support is not enabled at compile + * time. If apparmor is enabled then hat change is attempted. + * + * As with many functions in the snap-confine tree, all errors result in + * process termination. + **/ +void +sc_maybe_aa_change_hat(struct sc_apparmor *apparmor, + const char *subprofile, unsigned long magic_token); + +#endif diff --git a/cmd/snap-confine/cookie-support-test.c b/cmd/snap-confine/cookie-support-test.c new file mode 100644 index 00000000..b5f382ca --- /dev/null +++ b/cmd/snap-confine/cookie-support-test.c @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "cookie-support.h" +#include "cookie-support.c" + +#include "../libsnap-confine-private/test-utils.h" + +#include +#include +#include +#include + +// Set alternate cookie directory +static void set_cookie_dir(const char *dir) +{ + sc_cookie_dir = dir; +} + +static void set_fake_cookie_dir(void) +{ + char *ctx_dir = NULL; + ctx_dir = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(ctx_dir); + g_test_queue_free(ctx_dir); + + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, ctx_dir); + g_test_queue_destroy((GDestroyNotify) set_cookie_dir, SC_COOKIE_DIR); + + set_cookie_dir(ctx_dir); +} + +static void create_dumy_cookie_file(const char *snap_name, + const char *dummy_cookie) +{ + char path[PATH_MAX] = { 0 }; + FILE *f; + int n; + + snprintf(path, sizeof(path), "%s/snap.%s", sc_cookie_dir, snap_name); + + f = fopen(path, "w"); + g_assert_nonnull(f); + + n = fwrite(dummy_cookie, 1, strlen(dummy_cookie), f); + g_assert_cmpint(n, ==, strlen(dummy_cookie)); + + fclose(f); +} + +static void test_cookie_get_from_snapd__successful(void) +{ + struct sc_error *err = NULL; + char *cookie; + + char *dummy = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijmnopqrst"; + + set_fake_cookie_dir(); + create_dumy_cookie_file("test-snap", dummy); + + cookie = sc_cookie_get_from_snapd("test-snap", &err); + g_assert_null(err); + g_assert_nonnull(cookie); + g_assert_cmpint(strlen(cookie), ==, 44); + g_assert_cmpstr(cookie, ==, dummy); +} + +static void test_cookie_get_from_snapd__nofile(void) +{ + struct sc_error *err = NULL; + char *cookie; + + set_fake_cookie_dir(); + + cookie = sc_cookie_get_from_snapd("test-snap2", &err); + g_assert_nonnull(err); + g_assert_cmpstr(sc_error_domain(err), ==, SC_ERRNO_DOMAIN); + g_assert_nonnull(strstr(sc_error_msg(err), "cannot open cookie file")); + g_assert_null(cookie); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/snap-cookie/cookie_get_from_snapd/successful", + test_cookie_get_from_snapd__successful); + g_test_add_func("/snap-cookie/cookie_get_from_snapd/no_cookie_file", + test_cookie_get_from_snapd__nofile); +} diff --git a/cmd/snap-confine/cookie-support.c b/cmd/snap-confine/cookie-support.c new file mode 100644 index 00000000..1d267fb7 --- /dev/null +++ b/cmd/snap-confine/cookie-support.c @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "cookie-support.h" + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#define SC_COOKIE_DIR "/var/lib/snapd/cookie" + +/** + * Effective value of SC_COOKIE_DIR + **/ +static const char *sc_cookie_dir = SC_COOKIE_DIR; + +char *sc_cookie_get_from_snapd(const char *snap_name, struct sc_error **errorp) +{ + char context_path[PATH_MAX] = { 0 }; + struct sc_error *err = NULL; + char *context = NULL; + + sc_must_snprintf(context_path, sizeof(context_path), "%s/snap.%s", + sc_cookie_dir, snap_name); + int fd SC_CLEANUP(sc_cleanup_close) = -1; + fd = open(context_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) { + err = + sc_error_init_from_errno(errno, + "warning: cannot open cookie file %s", + context_path); + goto out; + } + // large enough buffer for opaque cookie string + char context_val[255] = { 0 }; + ssize_t n = read(fd, context_val, sizeof(context_val) - 1); + if (n < 0) { + err = + sc_error_init_from_errno(errno, + "cannot read cookie file %s", + context_path); + goto out; + } + context = strndup(context_val, n); + if (context == NULL) { + die("cannot duplicate snap cookie value"); + } + + out: + sc_error_forward(errorp, err); + return context; +} diff --git a/cmd/snap-confine/cookie-support.h b/cmd/snap-confine/cookie-support.h new file mode 100644 index 00000000..49774b0c --- /dev/null +++ b/cmd/snap-confine/cookie-support.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_CONTEXT_SUPPORT_H +#define SNAP_CONFINE_CONTEXT_SUPPORT_H + +#include "../libsnap-confine-private/error.h" + +/** + * Return snap cookie string for given snap. + * + * The context value is read from /var/lib/snapd/cookie/snap. + * file. The caller of the function takes the ownership of the returned cookie + * string. + * If the file cannot be read then an error is returned in errorp and + * the function returns NULL. + **/ +char *sc_cookie_get_from_snapd(const char *snap_name, struct sc_error **errorp); + +#endif diff --git a/cmd/snap-confine/misc/0001-Add-printk-based-debugging-to-pivot_root.patch b/cmd/snap-confine/misc/0001-Add-printk-based-debugging-to-pivot_root.patch new file mode 100644 index 00000000..225a47b8 --- /dev/null +++ b/cmd/snap-confine/misc/0001-Add-printk-based-debugging-to-pivot_root.patch @@ -0,0 +1,132 @@ +From 1ef45eb31cacd58c4c62e1fd26aa63a1f3d031a7 Mon Sep 17 00:00:00 2001 +From: Zygmunt Krynicki +Date: Thu, 29 Sep 2016 15:11:15 +0200 +Subject: [PATCH] Add printk-based debugging to pivot_root + +This patch changes pivot_root to make it obvious which error exit path +was taken. It might be useful to apply to debug and investigate how +undocumented requirements of pivot_root are not met. + +Signed-off-by: Zygmunt Krynicki +--- + fs/namespace.c | 70 ++++++++++++++++++++++++++++++++++++++++++++-------------- + 1 file changed, 53 insertions(+), 17 deletions(-) + +diff --git a/fs/namespace.c b/fs/namespace.c +index 877fc2c..6e15d1d 100644 +--- a/fs/namespace.c ++++ b/fs/namespace.c +@@ -2993,57 +2993,93 @@ SYSCALL_DEFINE2(pivot_root, const char __user *, new_root, + return -EPERM; + + error = user_path_dir(new_root, &new); +- if (error) ++ if (error) { ++ printk(KERN_ERR "user_path_dir(new_root, &new) returned an error\n"); + goto out0; ++ } + + error = user_path_dir(put_old, &old); +- if (error) ++ if (error) { ++ printk(KERN_ERR "user_path_dir(put_old, &old) returned an error\n"); + goto out1; ++ } + + error = security_sb_pivotroot(&old, &new); +- if (error) ++ if (error) { ++ printk(KERN_ERR "security_sb_pivotroot(&old, &new) returned an error\n"); + goto out2; ++ } + + get_fs_root(current->fs, &root); + old_mp = lock_mount(&old); + error = PTR_ERR(old_mp); +- if (IS_ERR(old_mp)) ++ if (IS_ERR(old_mp)) { ++ printk(KERN_ERR "IS_ERR(old_mp)\n"); + goto out3; ++ } + + error = -EINVAL; + new_mnt = real_mount(new.mnt); + root_mnt = real_mount(root.mnt); + old_mnt = real_mount(old.mnt); +- if (IS_MNT_SHARED(old_mnt) || +- IS_MNT_SHARED(new_mnt->mnt_parent) || +- IS_MNT_SHARED(root_mnt->mnt_parent)) ++ if (IS_MNT_SHARED(old_mnt)) { ++ printk(KERN_ERR "IS_MNT_SHARED(old_mnt)\n"); ++ goto out4; ++ } ++ if (IS_MNT_SHARED(new_mnt->mnt_parent)) { ++ printk(KERN_ERR "IS_MNT_SHARED(new_mnt->mnt_parent)\n"); + goto out4; +- if (!check_mnt(root_mnt) || !check_mnt(new_mnt)) ++ } ++ if (IS_MNT_SHARED(root_mnt->mnt_parent)) { ++ printk(KERN_ERR "IS_MNT_SHARED(root_mnt->mnt_parent)\n"); + goto out4; +- if (new_mnt->mnt.mnt_flags & MNT_LOCKED) ++ } ++ if (!check_mnt(root_mnt) || !check_mnt(new_mnt)) { ++ printk(KERN_ERR "!check_mnt(root_mnt) || !check_mnt(new_mnt)\n"); ++ goto out4; ++ } ++ if (new_mnt->mnt.mnt_flags & MNT_LOCKED) { ++ printk(KERN_ERR "new_mnt->mnt.mnt_flags & MNT_LOCKED\n"); + goto out4; ++ } + error = -ENOENT; +- if (d_unlinked(new.dentry)) ++ if (d_unlinked(new.dentry)) { ++ printk(KERN_ERR "d_unlinked(new.dentry)\n"); + goto out4; ++ } + error = -EBUSY; +- if (new_mnt == root_mnt || old_mnt == root_mnt) ++ if (new_mnt == root_mnt || old_mnt == root_mnt) { ++ printk(KERN_ERR "new_mnt == root_mnt || old_mnt == root_mnt\n"); + goto out4; /* loop, on the same file system */ ++ } + error = -EINVAL; +- if (root.mnt->mnt_root != root.dentry) ++ if (root.mnt->mnt_root != root.dentry) { ++ printk(KERN_ERR "root.mnt->mnt_root != root.dentry\n"); + goto out4; /* not a mountpoint */ +- if (!mnt_has_parent(root_mnt)) ++ } ++ if (!mnt_has_parent(root_mnt)) { ++ printk(KERN_ERR "!mnt_has_parent(root_mnt)\n"); + goto out4; /* not attached */ ++ } + root_mp = root_mnt->mnt_mp; +- if (new.mnt->mnt_root != new.dentry) ++ if (new.mnt->mnt_root != new.dentry) { ++ printk(KERN_ERR "new.mnt->mnt_root != new.dentry\n"); + goto out4; /* not a mountpoint */ +- if (!mnt_has_parent(new_mnt)) ++ } ++ if (!mnt_has_parent(new_mnt)) { ++ printk(KERN_ERR "!mnt_has_parent(new_mnt)\n"); + goto out4; /* not attached */ ++ } + /* make sure we can reach put_old from new_root */ +- if (!is_path_reachable(old_mnt, old.dentry, &new)) ++ if (!is_path_reachable(old_mnt, old.dentry, &new)) { ++ printk(KERN_ERR "!is_path_reachable(old_mnt, old.dentry, &new)\n"); + goto out4; ++ } + /* make certain new is below the root */ +- if (!is_path_reachable(new_mnt, new.dentry, &root)) ++ if (!is_path_reachable(new_mnt, new.dentry, &root)) { ++ printk(KERN_ERR "!is_path_reachable(new_mnt, new.dentry, &root)\n"); + goto out4; ++ } + root_mp->m_count++; /* pin it so it won't go away */ + lock_mount_hash(); + detach_mnt(new_mnt, &parent_path); +-- +2.7.4 + diff --git a/cmd/snap-confine/mount-support-nvidia.c b/cmd/snap-confine/mount-support-nvidia.c new file mode 100644 index 00000000..42f56c3d --- /dev/null +++ b/cmd/snap-confine/mount-support-nvidia.c @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "config.h" +#include "mount-support-nvidia.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +#define SC_NVIDIA_DRIVER_VERSION_FILE "/sys/module/nvidia/version" + +// note: if the parent dir changes to something other than +// the current /var/lib/snapd/lib then sc_mkdir_and_mount_and_bind +// and sc_mkdir_and_mount_and_bind need updating. +#define SC_LIBGL_DIR "/var/lib/snapd/lib/gl" +#define SC_LIBGL32_DIR "/var/lib/snapd/lib/gl32" +#define SC_VULKAN_DIR "/var/lib/snapd/lib/vulkan" + +// Location for NVIDIA vulkan files (including _wayland) +static const char *vulkan_globs[] = { + "/usr/share/vulkan/icd.d/*nvidia*.json", +}; + +static const size_t vulkan_globs_len = + sizeof vulkan_globs / sizeof *vulkan_globs; + +#ifdef NVIDIA_BIARCH + +// List of globs that describe nvidia userspace libraries. +// This list was compiled from the following packages. +// +// https://www.archlinux.org/packages/extra/x86_64/nvidia-304xx-libgl/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-304xx-utils/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-340xx-libgl/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-340xx-utils/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-libgl/files/ +// https://www.archlinux.org/packages/extra/x86_64/nvidia-utils/files/ +// +// FIXME: this doesn't yet work with libGLX and libglvnd redirector +// FIXME: this still doesn't work with the 361 driver +static const char *nvidia_globs[] = { + "/usr/lib/libEGL.so*", + "/usr/lib/libEGL_nvidia.so*", + "/usr/lib/libGL.so*", + "/usr/lib/libOpenGL.so*", + "/usr/lib/libGLESv1_CM.so*", + "/usr/lib/libGLESv1_CM_nvidia.so*", + "/usr/lib/libGLESv2.so*", + "/usr/lib/libGLESv2_nvidia.so*", + "/usr/lib/libGLX_indirect.so*", + "/usr/lib/libGLX_nvidia.so*", + "/usr/lib/libGLX.so*", + "/usr/lib/libGLdispatch.so*", + "/usr/lib/libGLU.so*", + "/usr/lib/libXvMCNVIDIA.so*", + "/usr/lib/libXvMCNVIDIA_dynamic.so*", + "/usr/lib/libcuda.so*", + "/usr/lib/libnvcuvid.so*", + "/usr/lib/libnvidia-cfg.so*", + "/usr/lib/libnvidia-compiler.so*", + "/usr/lib/libnvidia-eglcore.so*", + "/usr/lib/libnvidia-egl-wayland*", + "/usr/lib/libnvidia-encode.so*", + "/usr/lib/libnvidia-fatbinaryloader.so*", + "/usr/lib/libnvidia-fbc.so*", + "/usr/lib/libnvidia-glcore.so*", + "/usr/lib/libnvidia-glsi.so*", + "/usr/lib/libnvidia-ifr.so*", + "/usr/lib/libnvidia-ml.so*", + "/usr/lib/libnvidia-ptxjitcompiler.so*", + "/usr/lib/libnvidia-tls.so*", + "/usr/lib/vdpau/libvdpau_nvidia.so*", +}; + +static const size_t nvidia_globs_len = + sizeof nvidia_globs / sizeof *nvidia_globs; + +// 32-bit variants of the NVIDIA driver libraries +static const char *nvidia_globs32[] = { + "/usr/lib32/libEGL.so*", + "/usr/lib32/libEGL_nvidia.so*", + "/usr/lib32/libGL.so*", + "/usr/lib32/libOpenGL.so*", + "/usr/lib32/libGLESv1_CM.so*", + "/usr/lib32/libGLESv1_CM_nvidia.so*", + "/usr/lib32/libGLESv2.so*", + "/usr/lib32/libGLESv2_nvidia.so*", + "/usr/lib32/libGLX_indirect.so*", + "/usr/lib32/libGLX_nvidia.so*", + "/usr/lib32/libGLX.so*", + "/usr/lib32/libGLdispatch.so*", + "/usr/lib32/libGLU.so*", + "/usr/lib32/libXvMCNVIDIA.so*", + "/usr/lib32/libXvMCNVIDIA_dynamic.so*", + "/usr/lib32/libcuda.so*", + "/usr/lib32/libnvcuvid.so*", + "/usr/lib32/libnvidia-cfg.so*", + "/usr/lib32/libnvidia-compiler.so*", + "/usr/lib32/libnvidia-eglcore.so*", + "/usr/lib32/libnvidia-encode.so*", + "/usr/lib32/libnvidia-fatbinaryloader.so*", + "/usr/lib32/libnvidia-fbc.so*", + "/usr/lib32/libnvidia-glcore.so*", + "/usr/lib32/libnvidia-glsi.so*", + "/usr/lib32/libnvidia-ifr.so*", + "/usr/lib32/libnvidia-ml.so*", + "/usr/lib32/libnvidia-ptxjitcompiler.so*", + "/usr/lib32/libnvidia-tls.so*", + "/usr/lib32/vdpau/libvdpau_nvidia.so*", +}; + +static const size_t nvidia_globs32_len = + sizeof nvidia_globs32 / sizeof *nvidia_globs32; + +#endif // ifdef NVIDIA_BIARCH + +// Populate libgl_dir with a symlink farm to files matching glob_list. +// +// The symbolic links are made in one of two ways. If the library found is a +// file a regular symlink "$libname" -> "/path/to/hostfs/$libname" is created. +// If the library is a symbolic link then relative links are kept as-is but +// absolute links are translated to have "/path/to/hostfs" up front so that +// they work after the pivot_root elsewhere. +static void sc_populate_libgl_with_hostfs_symlinks(const char *libgl_dir, + const char *glob_list[], + size_t glob_list_len) +{ + glob_t glob_res SC_CLEANUP(globfree) = { + .gl_pathv = NULL}; + // Find all the entries matching the list of globs + for (size_t i = 0; i < glob_list_len; ++i) { + const char *glob_pattern = glob_list[i]; + int err = + glob(glob_pattern, i ? GLOB_APPEND : 0, NULL, &glob_res); + // Not all of the files have to be there (they differ depending on the + // driver version used). Ignore all errors that are not GLOB_NOMATCH. + if (err != 0 && err != GLOB_NOMATCH) { + die("cannot search using glob pattern %s: %d", + glob_pattern, err); + } + } + // Symlink each file found + for (size_t i = 0; i < glob_res.gl_pathc; ++i) { + char symlink_name[512] = { 0 }; + char symlink_target[512] = { 0 }; + const char *pathname = glob_res.gl_pathv[i]; + char *pathname_copy + SC_CLEANUP(sc_cleanup_string) = strdup(pathname); + char *filename = basename(pathname_copy); + struct stat stat_buf; + int err = lstat(pathname, &stat_buf); + if (err != 0) { + die("cannot stat file %s", pathname); + } + switch (stat_buf.st_mode & S_IFMT) { + case S_IFLNK:; + // Read the target of the symbolic link + char hostfs_symlink_target[512]; + ssize_t num_read; + hostfs_symlink_target[0] = 0; + num_read = + readlink(pathname, hostfs_symlink_target, + sizeof hostfs_symlink_target); + if (num_read == -1) { + die("cannot read symbolic link %s", pathname); + } + hostfs_symlink_target[num_read] = 0; + if (hostfs_symlink_target[0] == '/') { + sc_must_snprintf(symlink_target, + sizeof symlink_target, + "/var/lib/snapd/hostfs%s", + hostfs_symlink_target); + } else { + // Keep relative symlinks as-is, so that they point to -> libfoo.so.0.123 + sc_must_snprintf(symlink_target, + sizeof symlink_target, "%s", + hostfs_symlink_target); + } + break; + case S_IFREG: + sc_must_snprintf(symlink_target, + sizeof symlink_target, + "/var/lib/snapd/hostfs%s", pathname); + break; + default: + debug("ignoring unsupported entry: %s", pathname); + continue; + } + sc_must_snprintf(symlink_name, sizeof symlink_name, + "%s/%s", libgl_dir, filename); + debug("creating symbolic link %s -> %s", symlink_name, + symlink_target); + if (symlink(symlink_target, symlink_name) != 0) { + die("cannot create symbolic link %s -> %s", + symlink_name, symlink_target); + } + } +} + +static void sc_mkdir_and_mount_and_glob_files(const char *rootfs_dir, + const char *tgt_dir, + const char *glob_list[], + size_t glob_list_len) +{ + // Bind mount a tmpfs on $rootfs_dir/$tgt_dir (i.e. /var/lib/snapd/lib/gl) + char buf[512] = { 0 }; + sc_must_snprintf(buf, sizeof(buf), "%s%s", rootfs_dir, tgt_dir); + const char *libgl_dir = buf; + + int res = mkdir(libgl_dir, 0755); + if (res != 0 && errno != EEXIST) { + die("cannot create tmpfs target %s", libgl_dir); + } + + debug("mounting tmpfs at %s", libgl_dir); + if (mount("none", libgl_dir, "tmpfs", MS_NODEV | MS_NOEXEC, NULL) != 0) { + die("cannot mount tmpfs at %s", libgl_dir); + }; + // Populate libgl_dir with symlinks to libraries from hostfs + sc_populate_libgl_with_hostfs_symlinks(libgl_dir, glob_list, + glob_list_len); + // Remount $tgt_dir (i.e. .../lib/gl) read only + debug("remounting tmpfs as read-only %s", libgl_dir); + if (mount(NULL, buf, NULL, MS_REMOUNT | MS_RDONLY, NULL) != 0) { + die("cannot remount %s as read-only", buf); + } +} + +#ifdef NVIDIA_BIARCH + +static void sc_mount_nvidia_driver_biarch(const char *rootfs_dir) +{ + sc_mkdir_and_mount_and_glob_files(rootfs_dir, SC_LIBGL_DIR, + nvidia_globs, nvidia_globs_len); + sc_mkdir_and_mount_and_glob_files(rootfs_dir, SC_LIBGL32_DIR, + nvidia_globs32, nvidia_globs32_len); +} + +#endif // ifdef NVIDIA_BIARCH + +#ifdef NVIDIA_MULTIARCH + +struct sc_nvidia_driver { + int major_version; + int minor_version; +}; + +static void sc_probe_nvidia_driver(struct sc_nvidia_driver *driver) +{ + FILE *file SC_CLEANUP(sc_cleanup_file) = NULL; + debug("opening file describing nvidia driver version"); + file = fopen(SC_NVIDIA_DRIVER_VERSION_FILE, "rt"); + if (file == NULL) { + if (errno == ENOENT) { + debug("nvidia driver version file doesn't exist"); + driver->major_version = 0; + driver->minor_version = 0; + return; + } + die("cannot open file describing nvidia driver version"); + } + // Driver version format is MAJOR.MINOR where both MAJOR and MINOR are + // integers. We can use sscanf to parse this data. + if (fscanf + (file, "%d.%d", &driver->major_version, + &driver->minor_version) != 2) { + die("cannot parse nvidia driver version string"); + } + debug("parsed nvidia driver version: %d.%d", driver->major_version, + driver->minor_version); +} + +static void sc_mkdir_and_mount_and_bind(const char *rootfs_dir, + const char *src_dir, + const char *tgt_dir) +{ + struct sc_nvidia_driver driver; + + // Probe sysfs to get the version of the driver that is currently inserted. + sc_probe_nvidia_driver(&driver); + + // If there's driver in the kernel then don't mount userspace. + if (driver.major_version == 0) { + return; + } + // Construct the paths for the driver userspace libraries + // and for the gl directory. + char src[PATH_MAX] = { 0 }; + char dst[PATH_MAX] = { 0 }; + sc_must_snprintf(src, sizeof src, "%s-%d", src_dir, + driver.major_version); + sc_must_snprintf(dst, sizeof dst, "%s%s", rootfs_dir, tgt_dir); + + // If there is no userspace driver available then don't try to mount it. + // This can happen for any number of reasons but one interesting one is + // that that snapd runs in a lxd container on a host that uses nvidia. In + // that case the container may not have the userspace library installed but + // the kernel will still have the module around. + if (access(src, F_OK) != 0) { + return; + } + int res = mkdir(dst, 0755); + if (res != 0 && errno != EEXIST) { + die("cannot create directory %s", dst); + } + // Bind mount the binary nvidia driver into $tgt_dir (i.e. /var/lib/snapd/lib/gl). + debug("bind mounting nvidia driver %s -> %s", src, dst); + if (mount(src, dst, NULL, MS_BIND, NULL) != 0) { + die("cannot bind mount nvidia driver %s -> %s", src, dst); + } +} + +static void sc_mount_nvidia_driver_multiarch(const char *rootfs_dir) +{ + // Attempt mount of both the native and 32-bit variants of the driver if they exist + sc_mkdir_and_mount_and_bind(rootfs_dir, "/usr/lib/nvidia", + SC_LIBGL_DIR); + sc_mkdir_and_mount_and_bind(rootfs_dir, "/usr/lib32/nvidia", + SC_LIBGL32_DIR); +} + +#endif // ifdef NVIDIA_MULTIARCH + +void sc_mount_nvidia_driver(const char *rootfs_dir) +{ + /* If NVIDIA module isn't loaded, don't attempt to mount the drivers */ + if (access(SC_NVIDIA_DRIVER_VERSION_FILE, F_OK) != 0) { + return; + } +#ifdef NVIDIA_MULTIARCH + sc_mount_nvidia_driver_multiarch(rootfs_dir); +#endif // ifdef NVIDIA_MULTIARCH +#ifdef NVIDIA_BIARCH + sc_mount_nvidia_driver_biarch(rootfs_dir); +#endif // ifdef NVIDIA_BIARCH + + // Common for both driver mechanisms + sc_mkdir_and_mount_and_glob_files(rootfs_dir, SC_VULKAN_DIR, + vulkan_globs, vulkan_globs_len); +} diff --git a/cmd/snap-confine/mount-support-nvidia.h b/cmd/snap-confine/mount-support-nvidia.h new file mode 100644 index 00000000..56ec893f --- /dev/null +++ b/cmd/snap-confine/mount-support-nvidia.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_MOUNT_SUPPORT_NVIDIA_H +#define SNAP_CONFINE_MOUNT_SUPPORT_NVIDIA_H + +/** + * Make the Nvidia driver from the classic distribution available in the snap + * execution environment. + * + * This function may be a no-op, depending on build-time configuration options. + * If enabled the behavior differs from one distribution to another because of + * differences in classic packaging and perhaps version of the Nvidia driver. + * This function is designed to be called before pivot_root() switched the root + * filesystem. + * + * On Ubuntu, there are several versions of the binary Nvidia driver. The + * drivers are all installed in /usr/lib/nvidia-$MAJOR_VERSION where + * MAJOR_VERSION is an integer like 304, 331, 340, 346, 352 or 361. The driver + * is located by inspecting /sys/modules/nvidia/version which contains the + * string "$MAJOR_VERSION.$MINOR_VERSION". The appropriate directory is then + * bind mounted to /var/lib/snapd/lib/gl relative relative to the location of + * the root filesystem directory provided as an argument. + * + * On Arch another approach is used. Because the actual driver installs a + * number of shared objects into /usr/lib, they cannot be bind mounted + * directly. Instead a tmpfs is mounted on /var/lib/snapd/lib/gl. The tmpfs is + * subsequently populated with symlinks that point to a number of files in the + * /usr/lib directory on the classic filesystem. After the pivot_root() call + * those symlinks rely on the /var/lib/snapd/hostfs directory as a "gateway". + **/ +void sc_mount_nvidia_driver(const char *rootfs_dir); + +#endif diff --git a/cmd/snap-confine/mount-support-test.c b/cmd/snap-confine/mount-support-test.c new file mode 100644 index 00000000..a57016a5 --- /dev/null +++ b/cmd/snap-confine/mount-support-test.c @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "mount-support.h" +#include "mount-support.c" +#include "mount-support-nvidia.h" +#include "mount-support-nvidia.c" + +#include + +static void replace_slashes_with_NUL(char *path, size_t len) +{ + for (size_t i = 0; i < len; i++) { + if (path[i] == '/') + path[i] = '\0'; + } +} + +static void test_get_nextpath__typical(void) +{ + char path[] = "/some/path"; + size_t offset = 0; + size_t fulllen = strlen(path); + + // Prepare path for usage with get_nextpath() by replacing + // all path separators with the NUL byte. + replace_slashes_with_NUL(path, fulllen); + + // Run get_nextpath a few times to see what happens. + char *result; + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, "some"); + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, "path"); + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, NULL); +} + +static void test_get_nextpath__weird(void) +{ + char path[] = "..///path"; + size_t offset = 0; + size_t fulllen = strlen(path); + + // Prepare path for usage with get_nextpath() by replacing + // all path separators with the NUL byte. + replace_slashes_with_NUL(path, fulllen); + + // Run get_nextpath a few times to see what happens. + char *result; + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, "path"); + result = get_nextpath(path, &offset, fulllen); + g_assert_cmpstr(result, ==, NULL); +} + +static void test_is_subdir(void) +{ + // Sensible exaples are sensible + g_assert_true(is_subdir("/dir/subdir", "/dir/")); + g_assert_true(is_subdir("/dir/subdir", "/dir")); + g_assert_true(is_subdir("/dir/", "/dir")); + g_assert_true(is_subdir("/dir", "/dir")); + // Also without leading slash + g_assert_true(is_subdir("dir/subdir", "dir/")); + g_assert_true(is_subdir("dir/subdir", "dir")); + g_assert_true(is_subdir("dir/", "dir")); + g_assert_true(is_subdir("dir", "dir")); + // Some more ideas + g_assert_true(is_subdir("//", "/")); + g_assert_true(is_subdir("/", "/")); + g_assert_true(is_subdir("", "")); + // but this is not true + g_assert_false(is_subdir("/", "/dir")); + g_assert_false(is_subdir("/rid", "/dir")); + g_assert_false(is_subdir("/different/dir", "/dir")); + g_assert_false(is_subdir("/", "")); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/mount/get_nextpath/typical", + test_get_nextpath__typical); + g_test_add_func("/mount/get_nextpath/weird", test_get_nextpath__weird); + g_test_add_func("/mount/is_subdir", test_is_subdir); +} diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c new file mode 100644 index 00000000..e78b9a05 --- /dev/null +++ b/cmd/snap-confine/mount-support.c @@ -0,0 +1,715 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "config.h" +#include "mount-support.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/mount-opt.h" +#include "../libsnap-confine-private/mountinfo.h" +#include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" +#include "mount-support-nvidia.h" +#include "quirks.h" + +#define MAX_BUF 1000 + +/*! + * The void directory. + * + * Snap confine moves to that directory in case it cannot retain the current + * working directory across the pivot_root call. + **/ +#define SC_VOID_DIR "/var/lib/snapd/void" + +// TODO: simplify this, after all it is just a tmpfs +// TODO: fold this into bootstrap +static void setup_private_mount(const char *snap_name) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char tmpdir[MAX_BUF] = { 0 }; + + // Create a 0700 base directory, this is the base dir that is + // protected from other users. + // + // Under that basedir, we put a 1777 /tmp dir that is then bind + // mounted for the applications to use + sc_must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid, + snap_name); + if (mkdtemp(tmpdir) == NULL) { + die("cannot create temporary directory essential for private /tmp"); + } + // now we create a 1777 /tmp inside our private dir + mode_t old_mask = umask(0); + char *d = strdup(tmpdir); + if (!d) { + die("cannot allocate memory for string copy"); + } + sc_must_snprintf(tmpdir, sizeof(tmpdir), "%s/tmp", d); + free(d); + + if (mkdir(tmpdir, 01777) != 0) { + die("cannot create temporary directory for private /tmp"); + } + umask(old_mask); + + // chdir to '/' since the mount won't apply to the current directory + char *pwd = get_current_dir_name(); + if (pwd == NULL) + die("cannot get current working directory"); + if (chdir("/") != 0) + die("cannot change directory to '/'"); + + // MS_BIND is there from linux 2.4 + sc_do_mount(tmpdir, "/tmp", NULL, MS_BIND, NULL); + // MS_PRIVATE needs linux > 2.6.11 + sc_do_mount("none", "/tmp", NULL, MS_PRIVATE, NULL); + // do the chown after the bind mount to avoid potential shenanigans + if (chown("/tmp/", uid, gid) < 0) { + die("cannot change ownership of /tmp"); + } + // chdir to original directory + if (chdir(pwd) != 0) + die("cannot change current working directory to the original directory"); + free(pwd); +} + +// TODO: fold this into bootstrap +static void setup_private_pts(void) +{ + // See https://www.kernel.org/doc/Documentation/filesystems/devpts.txt + // + // Ubuntu by default uses devpts 'single-instance' mode where + // /dev/pts/ptmx is mounted with ptmxmode=0000. We don't want to change + // the startup scripts though, so we follow the instructions in point + // '4' of 'User-space changes' in the above doc. In other words, after + // unshare(CLONE_NEWNS), we mount devpts with -o + // newinstance,ptmxmode=0666 and then bind mount /dev/pts/ptmx onto + // /dev/ptmx + + struct stat st; + + // Make sure /dev/pts/ptmx exists, otherwise we are in legacy mode + // which doesn't provide the isolation we require. + if (stat("/dev/pts/ptmx", &st) != 0) { + die("cannot stat /dev/pts/ptmx"); + } + // Make sure /dev/ptmx exists so we can bind mount over it + if (stat("/dev/ptmx", &st) != 0) { + die("cannot stat /dev/ptmx"); + } + // Since multi-instance, use ptmxmode=0666. The other options are + // copied from /etc/default/devpts + sc_do_mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL, + "newinstance,ptmxmode=0666,mode=0620,gid=5"); + sc_do_mount("/dev/pts/ptmx", "/dev/ptmx", "none", MS_BIND, 0); +} + +/** + * Setup mount profiles by running snap-update-ns. + * + * The first argument is an open file descriptor (though opened with O_PATH, so + * not as powerful), to a copy of snap-update-ns. The program is opened before + * the root filesystem is pivoted so that it is easier to pick the right copy. + **/ +static void sc_setup_mount_profiles(int snap_update_ns_fd, + const char *snap_name) +{ + debug("calling snap-update-ns to initialize mount namespace"); + pid_t child = fork(); + if (child < 0) { + die("cannot fork to run snap-update-ns"); + } + if (child == 0) { + // We are the child, execute snap-update-ns + char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; + snap_name_copy = strdup(snap_name); + if (snap_name_copy == NULL) { + die("cannot copy snap name"); + } + char *argv[] = { + "snap-update-ns", "--from-snap-confine", snap_name_copy, + NULL + }; + char *envp[3] = { NULL }; + if (sc_is_debug_enabled()) { + envp[0] = "SNAPD_DEBUG=1"; + } + debug("fexecv(%d (snap-update-ns), %s %s %s,)", + snap_update_ns_fd, argv[0], argv[1], argv[2]); + fexecve(snap_update_ns_fd, argv, envp); + die("cannot execute snap-update-ns"); + } + // We are the parent, so wait for snap-update-ns to finish. + int status = 0; + debug("waiting for snap-update-ns to finish..."); + if (waitpid(child, &status, 0) < 0) { + die("waitpid() failed for snap-update-ns process"); + } + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { + die("snap-update-ns failed with code %i", WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + die("snap-update-ns killed by signal %i", WTERMSIG(status)); + } + debug("snap-update-ns finished successfully"); +} + +struct sc_mount { + const char *path; + bool is_bidirectional; +}; + +struct sc_mount_config { + const char *rootfs_dir; + // The struct is terminated with an entry with NULL path. + const struct sc_mount *mounts; + bool on_classic_distro; + bool uses_base_snap; +}; + +/** + * Bootstrap mount namespace. + * + * This is a chunk of tricky code that lets us have full control over the + * layout and direction of propagation of mount events. The documentation below + * assumes knowledge of the 'sharedsubtree.txt' document from the kernel source + * tree. + * + * As a reminder two definitions are quoted below: + * + * A 'propagation event' is defined as event generated on a vfsmount + * that leads to mount or unmount actions in other vfsmounts. + * + * A 'peer group' is defined as a group of vfsmounts that propagate + * events to each other. + * + * (end of quote). + * + * The main idea is to setup a mount namespace that has a root filesystem with + * vfsmounts and peer groups that, depending on the location, either isolate + * or share with the rest of the system. + * + * The vast majority of the filesystem is shared in one direction. Events from + * the outside (from the main mount namespace) propagate inside (to namespaces + * of particular snaps) so things like new snap revisions, mounted drives, etc, + * just show up as expected but even if a snap is exploited or malicious in + * nature it cannot affect anything in another namespace where it might cause + * security or stability issues. + * + * Selected directories (today just /media) can be shared in both directions. + * This allows snaps with sufficient privileges to either create, through the + * mount system call, additional mount points that are visible by the rest of + * the system (both the main mount namespace and namespaces of individual + * snaps) or remove them, through the unmount system call. + **/ +static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) +{ + char scratch_dir[] = "/tmp/snap.rootfs_XXXXXX"; + char src[PATH_MAX] = { 0 }; + char dst[PATH_MAX] = { 0 }; + if (mkdtemp(scratch_dir) == NULL) { + die("cannot create temporary directory for the root file system"); + } + // NOTE: at this stage we just called unshare(CLONE_NEWNS). We are in a new + // mount namespace and have a private list of mounts. + debug("scratch directory for constructing namespace: %s", scratch_dir); + // Make the root filesystem recursively shared. This way propagation events + // will be shared with main mount namespace. + sc_do_mount("none", "/", NULL, MS_REC | MS_SHARED, NULL); + // Bind mount the temporary scratch directory for root filesystem over + // itself so that it is a mount point. This is done so that it can become + // unbindable as explained below. + sc_do_mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL); + // Make the scratch directory unbindable. + // + // This is necessary as otherwise a mount loop can occur and the kernel + // would crash. The term unbindable simply states that it cannot be bind + // mounted anywhere. When we construct recursive bind mounts below this + // guarantees that this directory will not be replicated anywhere. + sc_do_mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL); + // Recursively bind mount desired root filesystem directory over the + // scratch directory. This puts the initial content into the scratch space + // and serves as a foundation for all subsequent operations below. + // + // The mount is recursive because it can either be applied to the root + // filesystem of a core system (aka all-snap) or the core snap on a classic + // system. In the former case we need recursive bind mounts to accurately + // replicate the state of the root filesystem into the scratch directory. + sc_do_mount(config->rootfs_dir, scratch_dir, NULL, MS_REC | MS_BIND, + NULL); + // Make the scratch directory recursively private. Nothing done there will + // be shared with any peer group, This effectively detaches us from the + // original namespace and coupled with pivot_root below serves as the + // foundation of the mount sandbox. + sc_do_mount("none", scratch_dir, NULL, MS_REC | MS_SLAVE, NULL); + // Bind mount certain directories from the host filesystem to the scratch + // directory. By default mount events will propagate in both into and out + // of the peer group. This way the running application can alter any global + // state visible on the host and in other snaps. This can be restricted by + // disabling the "is_bidirectional" flag as can be seen below. + for (const struct sc_mount * mnt = config->mounts; mnt->path != NULL; + mnt++) { + if (mnt->is_bidirectional && mkdir(mnt->path, 0755) < 0 && + errno != EEXIST) { + die("cannot create %s", mnt->path); + } + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, + mnt->path); + sc_do_mount(mnt->path, dst, NULL, MS_REC | MS_BIND, NULL); + if (!mnt->is_bidirectional) { + // Mount events will only propagate inwards to the namespace. This + // way the running application cannot alter any global state apart + // from that of its own snap. + sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL); + } + } + if (config->on_classic_distro) { + // Since we mounted /etc from the host filesystem to the scratch directory, + // we may need to put certain directories from the desired root filesystem + // (e.g. the core snap) back. This way the behavior of running snaps is not + // affected by the alternatives directory from the host, if one exists. + // + // Fixes the following bugs: + // - https://bugs.launchpad.net/snap-confine/+bug/1580018 + // - https://bugzilla.opensuse.org/show_bug.cgi?id=1028568 + const char *dirs_from_core[] = + { "/etc/alternatives", "/etc/ssl", "/etc/nsswitch.conf", + NULL + }; + for (const char **dirs = dirs_from_core; *dirs != NULL; dirs++) { + const char *dir = *dirs; + struct stat buf; + if (access(dir, F_OK) == 0) { + sc_must_snprintf(src, sizeof src, "%s%s", + config->rootfs_dir, dir); + sc_must_snprintf(dst, sizeof dst, "%s%s", + scratch_dir, dir); + if (lstat(src, &buf) == 0 + && lstat(dst, &buf) == 0) { + sc_do_mount(src, dst, NULL, MS_BIND, + NULL); + sc_do_mount("none", dst, NULL, MS_SLAVE, + NULL); + } + } + } + } + if (config->uses_base_snap) { + // when bases are used we need to bind-mount the libexecdir + // (that contains snap-exec) into /usr/lib/snapd of the + // base snap so that snap-exec is available for the snaps + // (base snaps do not ship snapd) + + // dst is always /usr/lib/snapd as this is where snapd + // assumes to find snap-exec + sc_must_snprintf(dst, sizeof dst, "%s/usr/lib/snapd", + scratch_dir); + + // bind mount the current $ROOT/usr/lib/snapd path, + // where $ROOT is either "/" or the "/snap/core/current" + // that we are re-execing from + char *src = NULL; + char self[PATH_MAX + 1] = { 0 }; + if (readlink("/proc/self/exe", self, sizeof(self) - 1) < 0) { + die("cannot read /proc/self/exe"); + } + // this cannot happen except when the kernel is buggy + if (strstr(self, "/snap-confine") == NULL) { + die("cannot use result from readlink: %s", src); + } + src = dirname(self); + // dirname(path) might return '.' depending on path. + // /proc/self/exe should always point + // to an absolute path, but let's guarantee that. + if (src[0] != '/') { + die("cannot use the result of dirname(): %s", src); + } + + sc_do_mount(src, dst, NULL, MS_BIND | MS_RDONLY, NULL); + sc_do_mount("none", dst, NULL, MS_SLAVE, NULL); + + // FIXME: snapctl tool - our apparmor policy wants it in + // /usr/bin/snapctl, we will need an empty file + // here from the base snap or we need to move it + // into a different location and just symlink it + // (/usr/lib/snapd/snapctl -> /usr/bin/snapctl) + // and in the base snap case adjust PATH + //src = "/usr/bin/snapctl"; + //sc_must_snprintf(dst, sizeof dst, "%s%s", scratch_dir, src); + //sc_do_mount(src, dst, NULL, MS_REC | MS_BIND, NULL); + //sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL); + } + // Bind mount the directory where all snaps are mounted. The location of + // the this directory on the host filesystem may not match the location in + // the desired root filesystem. In the "core" and "ubuntu-core" snaps the + // directory is always /snap. On the host it is a build-time configuration + // option stored in SNAP_MOUNT_DIR. + sc_must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir); + sc_do_mount(SNAP_MOUNT_DIR, dst, NULL, MS_BIND | MS_REC | MS_SLAVE, + NULL); + sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL); + // Create the hostfs directory if one is missing. This directory is a part + // of packaging now so perhaps this code can be removed later. + if (access(SC_HOSTFS_DIR, F_OK) != 0) { + debug("creating missing hostfs directory"); + if (mkdir(SC_HOSTFS_DIR, 0755) != 0) { + die("cannot perform operation: mkdir %s", + SC_HOSTFS_DIR); + } + } + // Ensure that hostfs isgroup owned by root. We may have (now or earlier) + // created the directory as the user who first ran a snap on a given + // system and the group identity of that user is visilbe on disk. + // This was LP:#1665004 + struct stat sb; + if (stat(SC_HOSTFS_DIR, &sb) < 0) { + die("cannot stat %s", SC_HOSTFS_DIR); + } + if (sb.st_uid != 0 || sb.st_gid != 0) { + if (chown(SC_HOSTFS_DIR, 0, 0) < 0) { + die("cannot change user/group owner of %s to root", + SC_HOSTFS_DIR); + } + } + // Make the upcoming "put_old" directory for pivot_root private so that + // mount events don't propagate to any peer group. In practice pivot root + // has a number of undocumented requirements and one of them is that the + // "put_old" directory (the second argument) cannot be shared in any way. + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, SC_HOSTFS_DIR); + sc_do_mount(dst, dst, NULL, MS_BIND, NULL); + sc_do_mount("none", dst, NULL, MS_PRIVATE, NULL); + // On classic mount the nvidia driver. Ideally this would be done in an + // uniform way after pivot_root but this is good enough and requires less + // code changes the nvidia code assumes it has access to the existing + // pre-pivot filesystem. + if (config->on_classic_distro) { + sc_mount_nvidia_driver(scratch_dir); + } + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + // pivot_root + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + // Use pivot_root to "chroot" into the scratch directory. + // + // Q: Why are we using something as esoteric as pivot_root(2)? + // A: Because this makes apparmor handling easy. Using a normal chroot + // makes all apparmor rules conditional. We are either running on an + // all-snap system where this would-be chroot didn't happen and all the + // rules see / as the root file system _OR_ we are running on top of a + // classic distribution and this chroot has now moved all paths to + // /tmp/snap.rootfs_*. + // + // Because we are using unshare(2) with CLONE_NEWNS we can essentially use + // pivot_root just like chroot but this makes apparmor unaware of the old + // root so everything works okay. + // + // HINT: If you are debugging this and are trying to see why pivot_root + // happens to return EINVAL with any changes you may be making, please + // consider applying + // misc/0001-Add-printk-based-debugging-to-pivot_root.patch to your tree + // kernel. + debug("performing operation: pivot_root %s %s", scratch_dir, dst); + if (syscall(SYS_pivot_root, scratch_dir, dst) < 0) { + die("cannot perform operation: pivot_root %s %s", scratch_dir, + dst); + } + // Unmount the self-bind mount over the scratch directory created earlier + // in the original root filesystem (which is now mounted on SC_HOSTFS_DIR). + // This way we can remove the temporary directory we created and "clean up" + // after ourselves nicely. + sc_must_snprintf(dst, sizeof dst, "%s/%s", SC_HOSTFS_DIR, scratch_dir); + sc_do_umount(dst, 0); + // Remove the scratch directory. Note that we are using the path that is + // based on the old root filesystem as after pivot_root we cannot guarantee + // what is present at the same location normally. (It is probably an empty + // /tmp directory that is populated in another place). + debug("performing operation: rmdir %s", dst); + if (rmdir(scratch_dir) < 0) { + die("cannot perform operation: rmdir %s", dst); + }; + // Make the old root filesystem recursively slave. This way operations + // performed in this mount namespace will not propagate to the peer group. + // This is another essential part of the confinement system. + sc_do_mount("none", SC_HOSTFS_DIR, NULL, MS_REC | MS_SLAVE, NULL); + // Detach the redundant hostfs version of sysfs since it shows up in the + // mount table and software inspecting the mount table may become confused + // (eg, docker and LP:# 162601). + sc_must_snprintf(src, sizeof src, "%s/sys", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); + // Detach the redundant hostfs version of /dev since it shows up in the + // mount table and software inspecting the mount table may become confused. + sc_must_snprintf(src, sizeof src, "%s/dev", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); + // Detach the redundant hostfs version of /proc since it shows up in the + // mount table and software inspecting the mount table may become confused. + sc_must_snprintf(src, sizeof src, "%s/proc", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); +} + +/** + * @path: a pathname where / replaced with '\0'. + * @offsetp: pointer to int showing which path segment was last seen. + * Updated on return to reflect the next segment. + * @fulllen: full original path length. + * Returns a pointer to the next path segment, or NULL if done. + */ +static char * __attribute__ ((used)) + get_nextpath(char *path, size_t * offsetp, size_t fulllen) +{ + size_t offset = *offsetp; + + if (offset >= fulllen) + return NULL; + + while (offset < fulllen && path[offset] != '\0') + offset++; + while (offset < fulllen && path[offset] == '\0') + offset++; + + *offsetp = offset; + return (offset < fulllen) ? &path[offset] : NULL; +} + +/** + * Check that @subdir is a subdir of @dir. +**/ +static bool __attribute__ ((used)) + is_subdir(const char *subdir, const char *dir) +{ + size_t dirlen = strlen(dir); + size_t subdirlen = strlen(subdir); + + // @dir has to be at least as long as @subdir + if (subdirlen < dirlen) + return false; + // @dir has to be a prefix of @subdir + if (strncmp(subdir, dir, dirlen) != 0) + return false; + // @dir can look like "path/" (that is, end with the directory separator). + // When that is the case then given the test above we can be sure @subdir + // is a real subdirectory. + if (dirlen > 0 && dir[dirlen - 1] == '/') + return true; + // @subdir can look like "path/stuff" and when the directory separator + // is exactly at the spot where @dir ends (that is, it was not caught + // by the test above) then @subdir is a real subdirectory. + if (subdir[dirlen] == '/' && dirlen > 0) + return true; + // If both @dir and @subdir have identical length then given that the + // prefix check above @subdir is a real subdirectory. + if (subdirlen == dirlen) + return true; + return false; +} + +static int sc_open_snap_update_ns(void) +{ + // +1 is for the case where the link is exactly PATH_MAX long but we also + // want to store the terminating '\0'. The readlink system call doesn't add + // terminating null, but our initialization of buf handles this for us. + char buf[PATH_MAX + 1] = { 0 }; + if (readlink("/proc/self/exe", buf, sizeof buf) < 0) { + die("cannot readlink /proc/self/exe"); + } + if (buf[0] != '/') { // this shouldn't happen, but make sure have absolute path + die("readlink /proc/self/exe returned relative path"); + } + char *bufcopy SC_CLEANUP(sc_cleanup_string) = NULL; + bufcopy = strdup(buf); + if (bufcopy == NULL) { + die("cannot copy buffer"); + } + char *dname = dirname(bufcopy); + sc_must_snprintf(buf, sizeof buf, "%s/%s", dname, "snap-update-ns"); + debug("snap-update-ns executable: %s", buf); + int fd = open(buf, O_PATH | O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) { + die("cannot open snap-update-ns executable"); + } + debug("opened snap-update-ns executable as file descriptor %d", fd); + return fd; +} + +void sc_populate_mount_ns(const char *base_snap_name, const char *snap_name) +{ + // Get the current working directory before we start fiddling with + // mounts and possibly pivot_root. At the end of the whole process, we + // will try to re-locate to the same directory (if possible). + char *vanilla_cwd SC_CLEANUP(sc_cleanup_string) = NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + // Find and open snap-update-ns from the same path as where we + // (snap-confine) were called. + int snap_update_ns_fd SC_CLEANUP(sc_cleanup_close) = -1; + snap_update_ns_fd = sc_open_snap_update_ns(); + + bool on_classic_distro = is_running_on_classic_distribution(); + // on classic or with alternative base snaps we need to setup + // a different confinement + if (on_classic_distro || !sc_streq(base_snap_name, "core")) { + const struct sc_mount mounts[] = { + {"/dev"}, // because it contains devices on host OS + {"/etc"}, // because that's where /etc/resolv.conf lives, perhaps a bad idea + {"/home"}, // to support /home/*/snap and home interface + {"/root"}, // because that is $HOME for services + {"/proc"}, // fundamental filesystem + {"/sys"}, // fundamental filesystem + {"/tmp"}, // to get writable tmp + {"/var/snap"}, // to get access to global snap data + {"/var/lib/snapd"}, // to get access to snapd state and seccomp profiles + {"/var/tmp"}, // to get access to the other temporary directory + {"/run"}, // to get /run with sockets and what not + {"/lib/modules"}, // access to the modules of the running kernel + {"/usr/src"}, // FIXME: move to SecurityMounts in system-trace interface + {"/var/log"}, // FIXME: move to SecurityMounts in log-observe interface +#ifdef MERGED_USR + {"/run/media", true}, // access to the users removable devices +#else + {"/media", true}, // access to the users removable devices +#endif // MERGED_USR + {"/run/netns", true}, // access to the 'ip netns' network namespaces + {}, + }; + char rootfs_dir[PATH_MAX] = { 0 }; + sc_must_snprintf(rootfs_dir, sizeof rootfs_dir, + "%s/%s/current/", SNAP_MOUNT_DIR, + base_snap_name); + if (access(rootfs_dir, F_OK) != 0) { + if (sc_streq(base_snap_name, "core")) { + // As a special fallback, allow the + // base snap to degrade from "core" to + // "ubuntu-core". This is needed for + // the migration tests. + base_snap_name = "ubuntu-core"; + sc_must_snprintf(rootfs_dir, sizeof rootfs_dir, + "%s/%s/current/", + SNAP_MOUNT_DIR, + base_snap_name); + if (access(rootfs_dir, F_OK) != 0) { + die("cannot locate the core or legacy core snap (current symlink missing?)"); + } + } + die("cannot locate the base snap: %s", base_snap_name); + } + struct sc_mount_config classic_config = { + .rootfs_dir = rootfs_dir, + .mounts = mounts, + .on_classic_distro = true, + .uses_base_snap = !sc_streq(base_snap_name, "core"), + }; + sc_bootstrap_mount_namespace(&classic_config); + } else { + // This is what happens on an all-snap system. The rootfs we start with + // is the real outer rootfs. There are no unidirectional bind mounts + // needed because everything is already OK. We still keep the + // bidirectional /media mount point so that snaps designed for mounting + // filesystems can use that space for whatever they need. + const struct sc_mount mounts[] = { + {"/media", true}, + {"/run/netns", true}, + {}, + }; + struct sc_mount_config all_snap_config = { + .rootfs_dir = "/", + .mounts = mounts, + .uses_base_snap = !sc_streq(base_snap_name, "core"), + }; + sc_bootstrap_mount_namespace(&all_snap_config); + } + + // set up private mounts + // TODO: rename this and fold it into bootstrap + setup_private_mount(snap_name); + + // set up private /dev/pts + // TODO: fold this into bootstrap + setup_private_pts(); + + // setup quirks for specific snaps + if (on_classic_distro) { + sc_setup_quirks(); + } + // setup the security backend bind mounts + sc_setup_mount_profiles(snap_update_ns_fd, snap_name); + + // Try to re-locate back to vanilla working directory. This can fail + // because that directory is no longer present. + if (chdir(vanilla_cwd) != 0) { + debug("cannot remain in %s, moving to the void directory", + vanilla_cwd); + if (chdir(SC_VOID_DIR) != 0) { + die("cannot change directory to %s", SC_VOID_DIR); + } + debug("successfully moved to %s", SC_VOID_DIR); + } +} + +static bool is_mounted_with_shared_option(const char *dir) + __attribute__ ((nonnull(1))); + +static bool is_mounted_with_shared_option(const char *dir) +{ + struct sc_mountinfo *sm SC_CLEANUP(sc_cleanup_mountinfo) = NULL; + sm = sc_parse_mountinfo(NULL); + if (sm == NULL) { + die("cannot parse /proc/self/mountinfo"); + } + struct sc_mountinfo_entry *entry = sc_first_mountinfo_entry(sm); + while (entry != NULL) { + const char *mount_dir = entry->mount_dir; + if (sc_streq(mount_dir, dir)) { + const char *optional_fields = entry->optional_fields; + if (strstr(optional_fields, "shared:") != NULL) { + return true; + } + } + entry = sc_next_mountinfo_entry(entry); + } + return false; +} + +void sc_ensure_shared_snap_mount(void) +{ + if (!is_mounted_with_shared_option("/") + && !is_mounted_with_shared_option(SNAP_MOUNT_DIR)) { + sc_do_mount(SNAP_MOUNT_DIR, SNAP_MOUNT_DIR, "none", + MS_BIND | MS_REC, 0); + sc_do_mount("none", SNAP_MOUNT_DIR, NULL, MS_SHARED | MS_REC, + NULL); + } +} diff --git a/cmd/snap-confine/mount-support.h b/cmd/snap-confine/mount-support.h new file mode 100644 index 00000000..ebeee838 --- /dev/null +++ b/cmd/snap-confine/mount-support.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_MOUNT_SUPPORT_H +#define SNAP_MOUNT_SUPPORT_H + +/** + * Assuming a new mountspace, populate it accordingly. + * + * This function performs many internal tasks: + * - prepares and chroots into the core snap (on classic systems) + * - creates private /tmp + * - creates private /dev/pts + * - applies quirks for specific snaps (like LXD) + * - processes mount profiles + * + * The function will also try to preserve the current working directory but if + * this is impossible it will chdir to SC_VOID_DIR. + **/ +void sc_populate_mount_ns(const char *base_snap_name, const char *snap_name); + +/** + * Ensure that / or /snap is mounted with the SHARED option. + * + * If the system is found to be not having a shared mount for "/" + * snap-confine will create a shared bind mount for "/snap" to + * ensure that "/snap" is mounted shared. See LP:#1668659 + */ +void sc_ensure_shared_snap_mount(void); +#endif diff --git a/cmd/snap-confine/ns-support-test.c b/cmd/snap-confine/ns-support-test.c new file mode 100644 index 00000000..542136cf --- /dev/null +++ b/cmd/snap-confine/ns-support-test.c @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "ns-support.h" +#include "ns-support.c" + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/test-utils.h" + +#include +#include // for NSFS_MAGIC +#include +#include + +#include +#include + +// Set alternate namespace directory +static void sc_set_ns_dir(const char *dir) +{ + sc_ns_dir = dir; +} + +// Use temporary directory for namespace groups. +// +// The directory is automatically reset to the real value at the end of the +// test. +static const char *sc_test_use_fake_ns_dir(void) +{ + char *ns_dir = NULL; + if (g_test_subprocess()) { + // Check if the environment variable is set. If so then someone is already + // managing the temporary directory and we should not create a new one. + ns_dir = getenv("SNAP_CONFINE_NS_DIR"); + g_assert_nonnull(ns_dir); + } else { + ns_dir = g_dir_make_tmp(NULL, NULL); + g_assert_nonnull(ns_dir); + g_test_queue_free(ns_dir); + g_assert_cmpint(setenv("SNAP_CONFINE_NS_DIR", ns_dir, 0), ==, + 0); + g_test_queue_destroy((GDestroyNotify) unsetenv, + "SNAP_CONFINE_NS_DIR"); + g_test_queue_destroy((GDestroyNotify) rm_rf_tmp, ns_dir); + } + g_test_queue_destroy((GDestroyNotify) sc_set_ns_dir, SC_NS_DIR); + sc_set_ns_dir(ns_dir); + return ns_dir; +} + +// Check that allocating a namespace group sets up internal data structures to +// safe values. +static void test_sc_alloc_ns_group(void) +{ + struct sc_ns_group *group = NULL; + group = sc_alloc_ns_group(); + g_test_queue_free(group); + g_assert_nonnull(group); + g_assert_cmpint(group->dir_fd, ==, -1); + g_assert_cmpint(group->event_fd, ==, -1); + g_assert_cmpint(group->child, ==, 0); + g_assert_cmpint(group->should_populate, ==, false); + g_assert_null(group->name); +} + +// Initialize a namespace group. +// +// The group is automatically destroyed at the end of the test. +static struct sc_ns_group *sc_test_open_ns_group(const char *group_name) +{ + // Initialize a namespace group + struct sc_ns_group *group = NULL; + if (group_name == NULL) { + group_name = "test-group"; + } + group = sc_open_ns_group(group_name, 0); + g_test_queue_destroy((GDestroyNotify) sc_close_ns_group, group); + // Check if the returned group data looks okay + g_assert_nonnull(group); + g_assert_cmpint(group->dir_fd, !=, -1); + g_assert_cmpint(group->event_fd, ==, -1); + g_assert_cmpint(group->child, ==, 0); + g_assert_cmpint(group->should_populate, ==, false); + g_assert_cmpstr(group->name, ==, group_name); + return group; +} + +// Check that initializing a namespace group creates the appropriate +// filesystem structure. +static void test_sc_open_ns_group(void) +{ + const char *ns_dir = sc_test_use_fake_ns_dir(); + sc_test_open_ns_group(NULL); + // Check that the group directory exists + g_assert_true(g_file_test + (ns_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)); +} + +static void test_sc_open_ns_group_graceful(void) +{ + sc_set_ns_dir("/nonexistent"); + g_test_queue_destroy((GDestroyNotify) sc_set_ns_dir, SC_NS_DIR); + struct sc_ns_group *group = + sc_open_ns_group("foo", SC_NS_FAIL_GRACEFULLY); + g_assert_null(group); +} + +static void unmount_dir(void *dir) +{ + umount(dir); +} + +static void test_sc_is_ns_group_dir_private(void) +{ + if (geteuid() != 0) { + g_test_skip("this test needs to run as root"); + return; + } + const char *ns_dir = sc_test_use_fake_ns_dir(); + g_test_queue_destroy(unmount_dir, (char *)ns_dir); + + if (g_test_subprocess()) { + // The temporary directory should not be private initially + g_assert_false(sc_is_ns_group_dir_private()); + + /// do what "mount --bind /foo /foo; mount --make-private /foo" does. + int err; + err = mount(ns_dir, ns_dir, NULL, MS_BIND, NULL); + g_assert_cmpint(err, ==, 0); + err = mount(NULL, ns_dir, NULL, MS_PRIVATE, NULL); + g_assert_cmpint(err, ==, 0); + + // The temporary directory should now be private + g_assert_true(sc_is_ns_group_dir_private()); + return; + } + g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed(); +} + +static void test_sc_initialize_ns_groups(void) +{ + if (geteuid() != 0) { + g_test_skip("this test needs to run as root"); + return; + } + // NOTE: this is g_test_subprocess aware! + const char *ns_dir = sc_test_use_fake_ns_dir(); + g_test_queue_destroy(unmount_dir, (char *)ns_dir); + if (g_test_subprocess()) { + // Initialize namespace groups using a fake directory. + sc_initialize_ns_groups(); + // Check that the fake directory is now a private mount. + g_assert_true(sc_is_ns_group_dir_private()); + return; + } + g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed(); +} + +// Sanity check, ensure that the namespace filesystem identifier is what we +// expect, aka NSFS_MAGIC. +static void test_nsfs_fs_id(void) +{ + struct utsname uts; + if (uname(&uts) < 0) { + g_test_message("cannot use uname(2)"); + g_test_fail(); + return; + } + int major, minor; + if (sscanf(uts.release, "%d.%d", &major, &minor) != 2) { + g_test_message("cannot use sscanf(2) to parse kernel release"); + g_test_fail(); + return; + } + if (major < 3 || (major == 3 && minor < 19)) { + g_test_skip("this test needs kernel 3.19+"); + return; + } + struct statfs buf; + int err = statfs("/proc/self/ns/mnt", &buf); + g_assert_cmpint(err, ==, 0); + g_assert_cmpint(buf.f_type, ==, NSFS_MAGIC); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/ns/sc_alloc_ns_group", test_sc_alloc_ns_group); + g_test_add_func("/ns/sc_open_ns_group", test_sc_open_ns_group); + g_test_add_func("/ns/sc_open_ns_group/graceful", + test_sc_open_ns_group_graceful); + g_test_add_func("/ns/nsfs_fs_id", test_nsfs_fs_id); + g_test_add_func("/system/ns/sc_is_ns_group_dir_private", + test_sc_is_ns_group_dir_private); + g_test_add_func("/system/ns/sc_initialize_ns_groups", + test_sc_initialize_ns_groups); +} diff --git a/cmd/snap-confine/ns-support.c b/cmd/snap-confine/ns-support.c new file mode 100644 index 00000000..e6d0587a --- /dev/null +++ b/cmd/snap-confine/ns-support.c @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "ns-support.h" + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/mountinfo.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" +#include "../libsnap-confine-private/locking.h" +#include "user-support.h" + +/*! + * The void directory. + * + * Snap confine moves to that directory in case it cannot retain the current + * working directory across the pivot_root call. + **/ +#define SC_VOID_DIR "/var/lib/snapd/void" + +/** + * Directory where snap-confine keeps namespace files. + **/ +#define SC_NS_DIR "/run/snapd/ns" + +/** + * Effective value of SC_NS_DIR. + * + * We use 'const char *' so we can update sc_ns_dir in the testsuite + **/ +static const char *sc_ns_dir = SC_NS_DIR; + +/** + * Name of the preserved mount namespace associated with SC_NS_DIR + * and a given group identifier (typically SNAP_NAME). + **/ +#define SC_NS_MNT_FILE ".mnt" + +/** + * Read /proc/self/mountinfo and check if /run/snapd/ns is a private bind mount. + * + * We do this because /run/snapd/ns cannot be shared with any other peers as per: + * https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt + **/ +static bool sc_is_ns_group_dir_private(void) +{ + struct sc_mountinfo *info SC_CLEANUP(sc_cleanup_mountinfo) = NULL; + info = sc_parse_mountinfo(NULL); + if (info == NULL) { + die("cannot parse /proc/self/mountinfo"); + } + struct sc_mountinfo_entry *entry = sc_first_mountinfo_entry(info); + while (entry != NULL) { + const char *mount_dir = entry->mount_dir; + const char *optional_fields = entry->optional_fields; + if (strcmp(mount_dir, sc_ns_dir) == 0 + && strcmp(optional_fields, "") == 0) { + // If /run/snapd/ns has no optional fields, we know it is mounted + // private and there is nothing else to do. + return true; + } + entry = sc_next_mountinfo_entry(entry); + } + return false; +} + +void sc_reassociate_with_pid1_mount_ns(void) +{ + int init_mnt_fd SC_CLEANUP(sc_cleanup_close) = -1; + int self_mnt_fd SC_CLEANUP(sc_cleanup_close) = -1; + + debug("checking if the current process shares mount namespace" + " with the init process"); + + init_mnt_fd = open("/proc/1/ns/mnt", + O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_PATH); + if (init_mnt_fd < 0) { + die("cannot open mount namespace of the init process (O_PATH)"); + } + self_mnt_fd = open("/proc/self/ns/mnt", + O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_PATH); + if (self_mnt_fd < 0) { + die("cannot open mount namespace of the current process (O_PATH)"); + } + char init_buf[128] = { 0 }; + char self_buf[128] = { 0 }; + memset(init_buf, 0, sizeof init_buf); + if (readlinkat(init_mnt_fd, "", init_buf, sizeof init_buf) < 0) { + if (errno == ENOENT) { + // According to namespaces(7) on a pre 3.8 kernel the namespace + // files are hardlinks, not sylinks. If that happens readlinkat + // fails with ENOENT. As a quick workaround for this special-case + // functionality, just bail out and do nothing without raising an + // error. + return; + } + die("cannot perform readlinkat() on the mount namespace file " + "descriptor of the init process"); + } + memset(self_buf, 0, sizeof self_buf); + if (readlinkat(self_mnt_fd, "", self_buf, sizeof self_buf) < 0) { + die("cannot perform readlinkat() on the mount namespace file " + "descriptor of the current process"); + } + if (memcmp(init_buf, self_buf, sizeof init_buf) != 0) { + debug("the current process does not share mount namespace with " + "the init process, re-association required"); + // NOTE: we cannot use O_NOFOLLOW here because that file will always be a + // symbolic link. We actually want to open it this way. + int init_mnt_fd_real SC_CLEANUP(sc_cleanup_close) = -1; + init_mnt_fd_real = open("/proc/1/ns/mnt", O_RDONLY | O_CLOEXEC); + if (init_mnt_fd_real < 0) { + die("cannot open mount namespace of the init process"); + } + if (setns(init_mnt_fd_real, CLONE_NEWNS) < 0) { + die("cannot re-associate the mount namespace with the init process"); + } + } else { + debug("re-associating is not required"); + } +} + +void sc_initialize_ns_groups(void) +{ + debug("creating namespace group directory %s", sc_ns_dir); + if (sc_nonfatal_mkpath(sc_ns_dir, 0755) < 0) { + die("cannot create namespace group directory %s", sc_ns_dir); + } + if (!sc_is_ns_group_dir_private()) { + debug + ("bind mounting the namespace group directory over itself"); + if (mount(sc_ns_dir, sc_ns_dir, NULL, MS_BIND | MS_REC, NULL) < + 0) { + die("cannot bind mount namespace group directory over itself"); + } + debug + ("making the namespace group directory mount point private"); + if (mount(NULL, sc_ns_dir, NULL, MS_PRIVATE, NULL) < 0) { + die("cannot make the namespace group directory mount point private"); + } + } else { + debug + ("namespace group directory does not require intialization"); + } +} + +struct sc_ns_group { + // Name of the namespace group ($SNAP_NAME). + char *name; + // Descriptor to the namespace group control directory. This descriptor is + // opened with O_PATH|O_DIRECTORY so it's only used for openat() calls. + int dir_fd; + // Descriptor to an eventfd that is used to notify the child that it can + // now complete its job and exit. + int event_fd; + // Identifier of the child process that is used during the one-time (per + // group) initialization and capture process. + pid_t child; + // Flag set when this process created a fresh namespace should populate it. + bool should_populate; +}; + +static struct sc_ns_group *sc_alloc_ns_group(void) +{ + struct sc_ns_group *group = calloc(1, sizeof *group); + if (group == NULL) { + die("cannot allocate memory for namespace group"); + } + group->dir_fd = -1; + group->event_fd = -1; + // Redundant with calloc but some functions check for the non-zero value so + // I'd like to keep this explicit in the code. + group->child = 0; + return group; +} + +struct sc_ns_group *sc_open_ns_group(const char *group_name, + const unsigned flags) +{ + struct sc_ns_group *group = sc_alloc_ns_group(); + debug("opening namespace group directory %s", sc_ns_dir); + group->dir_fd = + open(sc_ns_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); + if (group->dir_fd < 0) { + if (flags & SC_NS_FAIL_GRACEFULLY && errno == ENOENT) { + free(group); + return NULL; + } + die("cannot open directory for namespace group %s", group_name); + } + group->name = strdup(group_name); + if (group->name == NULL) { + die("cannot duplicate namespace group name %s", group_name); + } + return group; +} + +void sc_close_ns_group(struct sc_ns_group *group) +{ + debug("releasing resources associated with namespace group %s", + group->name); + sc_cleanup_close(&group->dir_fd); + sc_cleanup_close(&group->event_fd); + free(group->name); + free(group); +} + +static dev_t find_base_snap_device(const char *base_snap_name, + const char *base_snap_rev) +{ + // Find the backing device of the base snap. + // TODO: add support for "try mode" base snaps that also need + // consideration of the mie->root component. + dev_t base_snap_dev = 0; + char base_squashfs_path[PATH_MAX]; + sc_must_snprintf(base_squashfs_path, + sizeof base_squashfs_path, "%s/%s/%s", + SNAP_MOUNT_DIR, base_snap_name, base_snap_rev); + struct sc_mountinfo *mi SC_CLEANUP(sc_cleanup_mountinfo) = NULL; + mi = sc_parse_mountinfo(NULL); + if (mi == NULL) { + die("cannot parse mountinfo of the current process"); + } + bool found = false; + for (struct sc_mountinfo_entry * mie = + sc_first_mountinfo_entry(mi); mie != NULL; + mie = sc_next_mountinfo_entry(mie)) { + if (sc_streq(mie->mount_dir, base_squashfs_path)) { + base_snap_dev = MKDEV(mie->dev_major, mie->dev_minor); + debug("found base snap filesystem device %d:%d", + mie->dev_major, mie->dev_minor); + // Don't break when found, we are interested in the last + // entry as this is the "effective" one. + found = true; + } + } + if (!found) { + die("cannot find device backing the base snap %s", + base_snap_name); + } + return base_snap_dev; +} + +static bool should_discard_current_ns(dev_t base_snap_dev) +{ + // Inspect the namespace and check if we should discard it. + // + // The namespace may become "stale" when the rootfs is not the same + // device we found above. This will happen whenever the base snap is + // refreshed since the namespace was first created. + struct sc_mountinfo_entry *mie; + struct sc_mountinfo *mi SC_CLEANUP(sc_cleanup_mountinfo) = NULL; + + mi = sc_parse_mountinfo(NULL); + if (mi == NULL) { + die("cannot parse mountinfo of the current process"); + } + for (mie = sc_first_mountinfo_entry(mi); mie != NULL; + mie = sc_next_mountinfo_entry(mie)) { + if (!sc_streq(mie->mount_dir, "/")) { + continue; + } + // NOTE: we want the initial rootfs just in case overmount + // was used to do something weird. The initial rootfs was + // set up by snap-confine and that is the one we want to + // measure. + debug("found root filesystem inside the mount namespace %d:%d", + mie->dev_major, mie->dev_minor); + return base_snap_dev != MKDEV(mie->dev_major, mie->dev_minor); + } + die("cannot find mount entry of the root filesystem inside snap namespace"); +} + +void sc_create_or_join_ns_group(struct sc_ns_group *group, + struct sc_apparmor *apparmor, + const char *base_snap_name, + const char *snap_name) +{ + // Open the mount namespace file. + char mnt_fname[PATH_MAX] = { 0 }; + sc_must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, + SC_NS_MNT_FILE); + int mnt_fd SC_CLEANUP(sc_cleanup_close) = -1; + // NOTE: There is no O_EXCL here because the file can be around but + // doesn't have to be a mounted namespace. + // + // If the mounted namespace is discarded with + // sc_discard_preserved_ns_group() it will revert to a regular file. If + // snap-confine is killed for whatever reason after the file is created but + // before the file is bind-mounted it will also be a regular file. + mnt_fd = openat(group->dir_fd, mnt_fname, + O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600); + if (mnt_fd < 0) { + die("cannot open mount namespace file for namespace group %s", + group->name); + } + // Check if we got an nsfs-based or procfs file or a regular file. This can + // be reliably tested because nsfs has an unique filesystem type + // NSFS_MAGIC. On older kernels that don't support nsfs yet we can look + // for PROC_SUPER_MAGIC instead. + // We can just ensure that this is the case thanks to fstatfs. + struct statfs ns_statfs_buf; + if (fstatfs(mnt_fd, &ns_statfs_buf) < 0) { + die("cannot perform fstatfs() on the mount namespace file descriptor"); + } + // Stat the mount namespace as well, this is later used to check if the + // namespace is used by other processes if we are considering discarding a + // stale namespace. + struct stat ns_stat_buf; + if (fstat(mnt_fd, &ns_stat_buf) < 0) { + die("cannot perform fstat() on the mount namespace file descriptor"); + } +#ifndef NSFS_MAGIC +// Account for kernel headers old enough to not know about NSFS_MAGIC. +#define NSFS_MAGIC 0x6e736673 +#endif + if (ns_statfs_buf.f_type == NSFS_MAGIC + || ns_statfs_buf.f_type == PROC_SUPER_MAGIC) { + char fname[PATH_MAX] = { 0 }; + char base_snap_rev[PATH_MAX] = { 0 }; + + // Read the revision of the base snap. + sc_must_snprintf(fname, sizeof fname, "%s/%s/current", + SNAP_MOUNT_DIR, base_snap_name); + if (readlink(fname, base_snap_rev, sizeof base_snap_rev) < 0) { + die("cannot read symlink %s", fname); + } + if (base_snap_rev[sizeof base_snap_rev - 1] != '\0') { + die("cannot use symbolic link %s - value is too long", + fname); + } + + dev_t base_snap_dev = + find_base_snap_device(base_snap_name, base_snap_rev); + + // Remember the vanilla working directory so that we may attempt to restore it later. + char *vanilla_cwd SC_CLEANUP(sc_cleanup_string) = NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + // Move to the mount namespace of the snap we're trying to start. + debug + ("attempting to re-associate the mount namespace with the namespace group %s", + group->name); + if (setns(mnt_fd, CLONE_NEWNS) < 0) { + die("cannot re-associate the mount namespace with namespace group %s", group->name); + } + debug + ("successfully re-associated the mount namespace with the namespace group %s", + group->name); + + bool should_discard_ns = + should_discard_current_ns(base_snap_dev); + + if (should_discard_ns) { + debug("discarding obsolete base filesystem namespace"); + debug("(not yet implemented)"); + } + // Try to re-locate back to vanilla working directory. This can fail + // because that directory is no longer present. + if (chdir(vanilla_cwd) != 0) { + debug + ("cannot remain in %s, moving to the void directory", + vanilla_cwd); + if (chdir(SC_VOID_DIR) != 0) { + die("cannot change directory to %s", + SC_VOID_DIR); + } + debug("successfully moved to %s", SC_VOID_DIR); + } + return; + } + debug("initializing new namespace group %s", group->name); + // Create a new namespace and ask the caller to populate it. + // For rationale of forking see this: + // https://lists.linuxfoundation.org/pipermail/containers/2013-August/033386.html + // + // The eventfd created here is used to synchronize the child and the parent + // processes. It effectively tells the child to perform the capture + // operation. + group->event_fd = eventfd(0, EFD_CLOEXEC); + if (group->event_fd < 0) { + die("cannot create eventfd for mount namespace capture"); + } + debug("forking support process for mount namespace capture"); + // Store the PID of the "parent" process. This done instead of calls to + // getppid() because then we can reliably track the PID of the parent even + // if the child process is re-parented. + pid_t parent = getpid(); + // Glibc defines pid as a signed 32bit integer. There's no standard way to + // print pid's portably so this is the best we can do. + pid_t pid = fork(); + debug("forked support process has pid %d", (int)pid); + if (pid < 0) { + die("cannot fork support process for mount namespace capture"); + } + if (pid == 0) { + // This is the child process which will capture the mount namespace. + // + // It will do so by bind-mounting the SC_NS_MNT_FILE after the parent + // process calls unshare() and finishes setting up the namespace + // completely. + // Change the hat to a sub-profile that has limited permissions + // necessary to accomplish the capture of the mount namespace. + debug + ("changing apparmor hat of the support process for mount namespace capture"); + sc_maybe_aa_change_hat(apparmor, + "mount-namespace-capture-helper", 0); + // Configure the child to die as soon as the parent dies. In an odd + // case where the parent is killed then we don't want to complete our + // task or wait for anything. + if (prctl(PR_SET_PDEATHSIG, SIGINT, 0, 0, 0) < 0) { + die("cannot set parent process death notification signal to SIGINT"); + } + // Check that parent process is still alive. If this is the case then + // we can *almost* reliably rely on the PR_SET_PDEATHSIG signal to wake + // us up from eventfd_read() below. In the rare case that the PID numbers + // overflow and the now-dead parent PID is recycled we will still hang + // forever on the read from eventfd below. + debug("ensuring that parent process is still alive"); + if (kill(parent, 0) < 0) { + switch (errno) { + case ESRCH: + debug("parent process has already terminated"); + abort(); + default: + die("cannot ensure that parent process is still alive"); + break; + } + } + if (fchdir(group->dir_fd) < 0) { + die("cannot move process for mount namespace capture to namespace group directory"); + } + debug + ("waiting for a eventfd data from the parent process to continue"); + eventfd_t value = 0; + sc_enable_sanity_timeout(); + if (eventfd_read(group->event_fd, &value) < 0) { + die("cannot read expected data from eventfd"); + } + sc_disable_sanity_timeout(); + debug + ("capturing mount namespace of process %d in namespace group %s", + (int)parent, group->name); + char src[PATH_MAX] = { 0 }; + char dst[PATH_MAX] = { 0 }; + sc_must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", + (int)parent); + sc_must_snprintf(dst, sizeof dst, "%s%s", group->name, + SC_NS_MNT_FILE); + if (mount(src, dst, NULL, MS_BIND, NULL) < 0) { + die("cannot bind-mount the mount namespace file %s -> %s", src, dst); + } + debug + ("successfully captured mount namespace in namespace group %s", + group->name); + exit(0); + } else { + group->child = pid; + // Unshare the mount namespace and set a flag instructing the caller that + // the namespace is pristine and needs to be populated now. + debug("unsharing the mount namespace"); + if (unshare(CLONE_NEWNS) < 0) { + die("cannot unshare the mount namespace"); + } + group->should_populate = true; + } +} + +bool sc_should_populate_ns_group(struct sc_ns_group *group) +{ + return group->should_populate; +} + +void sc_preserve_populated_ns_group(struct sc_ns_group *group) +{ + if (group->child == 0) { + die("precondition failed: we don't have a support process for mount namespace capture"); + } + if (group->event_fd < 0) { + die("precondition failed: we don't have an eventfd for mount namespace capture"); + } + debug + ("asking support process for mount namespace capture (pid: %d) to perform the capture", + group->child); + if (eventfd_write(group->event_fd, 1) < 0) { + die("cannot write eventfd"); + } + debug + ("waiting for the support process for mount namespace capture to exit"); + int status = 0; + errno = 0; + if (waitpid(group->child, &status, 0) < 0) { + die("cannot wait for the support process for mount namespace capture"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + die("support process for mount namespace capture exited abnormally"); + } + debug("support process for mount namespace capture exited normally"); + group->child = 0; +} + +void sc_discard_preserved_ns_group(struct sc_ns_group *group) +{ + // Remember the current working directory + int old_dir_fd SC_CLEANUP(sc_cleanup_close) = -1; + old_dir_fd = open(".", O_PATH | O_DIRECTORY | O_CLOEXEC); + if (old_dir_fd < 0) { + die("cannot open current directory"); + } + // Move to the mount namespace directory (/run/snapd/ns) + if (fchdir(group->dir_fd) < 0) { + die("cannot move to namespace group directory"); + } + // Unmount ${group_name}.mnt which holds the preserved namespace + char mnt_fname[PATH_MAX] = { 0 }; + sc_must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, + SC_NS_MNT_FILE); + debug("unmounting preserved mount namespace file %s", mnt_fname); + if (umount2(mnt_fname, UMOUNT_NOFOLLOW) < 0) { + switch (errno) { + case EINVAL: + // EINVAL is returned when there's nothing to unmount (no bind-mount). + // Instead of checking for this explicitly (which is always racy) we + // just unmount and check the return code. + break; + case ENOENT: + // We may be asked to discard a namespace that doesn't yet + // exist (even the mount point may be absent). We just + // ignore that error and return gracefully. + break; + default: + die("cannot unmount preserved mount namespace file %s", + mnt_fname); + break; + } + } + // Get back to the original directory + if (fchdir(old_dir_fd) < 0) { + die("cannot move back to original directory"); + } +} diff --git a/cmd/snap-confine/ns-support.h b/cmd/snap-confine/ns-support.h new file mode 100644 index 00000000..bf2c8dcf --- /dev/null +++ b/cmd/snap-confine/ns-support.h @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_NAMESPACE_SUPPORT +#define SNAP_NAMESPACE_SUPPORT + +#include + +#include "apparmor-support.h" + +/** + * Re-associate the current process with the mount namespace of pid 1. + * + * This function inspects the mount namespace of the current process and that + * of pid 1. In case they differ the current process is re-associated with the + * mount namespace of pid 1. + * + * This function should be called before sc_initialize_ns_groups(). + **/ +void sc_reassociate_with_pid1_mount_ns(void); + +/** + * Initialize namespace sharing. + * + * This function must be called once in each process that wishes to create or + * join a namespace group. + * + * It is responsible for bind mounting the control directory over itself and + * making it private (unsharing it with all the other peers) so that it can be + * used for storing preserved namespaces as bind-mounted files from the nsfs + * filesystem (namespace filesystem). + * + * This function should be called with a global lock (see sc_lock_global) held + * to ensure that no other instance of snap-confine attempts to do this + * concurrently. + * + * This function inspects /proc/self/mountinfo to determine if the directory + * where namespaces are kept (/run/snapd/ns) is correctly prepared as described + * above. + * + * For more details see namespaces(7). + **/ +void sc_initialize_ns_groups(void); + +/** + * Data required to manage namespaces amongst a group of processes. + */ +struct sc_ns_group; + +enum { + SC_NS_FAIL_GRACEFULLY = 1 +}; + +/** + * Open a namespace group. + * + * This will open and keep file descriptors for /run/snapd/ns/. + * + * If the flags argument is SC_NS_FAIL_GRACEFULLY then the function returns + * NULL if the /run/snapd/ns directory doesn't exist. In all other cases it + * calls die() and exits the process. + * + * The following methods should be called only while holding a lock protecting + * that specific snap namespace: + * - sc_create_or_join_ns_group() + * - sc_should_populate_ns_group() + * - sc_preserve_populated_ns_group() + * - sc_discard_preserved_ns_group() + */ +struct sc_ns_group *sc_open_ns_group(const char *group_name, + const unsigned flags); + +/** + * Close namespace group. + * + * This will close all of the open file descriptors and release allocated memory. + */ +void sc_close_ns_group(struct sc_ns_group *group); + +/** + * Join the mount namespace associated with this group if one exists. + * + * Technically the function opens /run/snapd/ns/${group_name}.mnt and tries to + * use setns() with the obtained file descriptor. If the call succeeds then the + * function returns and subsequent call to sc_should_populate_ns_group() will + * return false. + * + * If the call fails then an eventfd is constructed and a support process is + * forked. The child process waits until data is written to the eventfd (this + * can be done by calling sc_preserve_populated_ns_group()). In the meantime + * the parent process unshares the mount namespace and sets a flag so that + * sc_should_populate_ns_group() returns true. + * + * @returns true if the mount namespace needs to be populated + **/ +void sc_create_or_join_ns_group(struct sc_ns_group *group, + struct sc_apparmor *apparmor, + const char *base_snap_name, + const char *snap_name); + +/** + * Check if the namespace needs to be populated. + * + * If the return value is true then at this stage the namespace is already + * unshared. The caller should perform any mount operations that are desired + * and then proceed to call sc_preserve_populated_ns_group(). + **/ +bool sc_should_populate_ns_group(struct sc_ns_group *group); + +/** + * Preserve prepared namespace group. + * + * This function signals the child support process for namespace capture to + * perform the capture and shut down. It must be called after the call to + * sc_create_or_join_ns_group() and only when sc_should_populate_ns_group() + * returns true. + * + * Technically this function writes to an eventfd that causes the child process + * to wake up, bind mount /proc/$ppid/ns/mnt to /run/snapd/ns/${group_name}.mnt + * and then exit. The parent process (the caller) then collects the child + * process and returns. + **/ +void sc_preserve_populated_ns_group(struct sc_ns_group *group); + +/** + * Discard the preserved namespace group. + * + * This function unmounts the bind-mounted files representing the kernel mount + * namespace. + **/ +void sc_discard_preserved_ns_group(struct sc_ns_group *group); + +#endif diff --git a/cmd/snap-confine/quirks.c b/cmd/snap-confine/quirks.c new file mode 100644 index 00000000..a7df7782 --- /dev/null +++ b/cmd/snap-confine/quirks.c @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "config.h" +#include "quirks.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/mount-opt.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" +// XXX: for smaller patch, this should be in utils.h later +#include "user-support.h" + +/** + * Get the path to the mounted core snap in the execution environment. + * + * The core snap may be named just "core" (preferred) or "ubuntu-core" + * (legacy). The mount point does not depend on build-time configuration and + * does not differ from distribution to distribution. + **/ +static const char *sc_get_inner_core_mount_point(void) +{ + const char *core_path = "/snap/core/current/"; + const char *ubuntu_core_path = "/snap/ubuntu-core/current/"; + static const char *result = NULL; + if (result == NULL) { + if (access(core_path, F_OK) == 0) { + // Use the "core" snap if available. + result = core_path; + } else if (access(ubuntu_core_path, F_OK) == 0) { + // If not try to fall back to the "ubuntu-core" snap. + result = ubuntu_core_path; + } else { + die("cannot locate the core snap"); + } + } + return result; +} + +/** + * Mount a tmpfs at a given directory. + * + * The empty tmpfs is used as a substrate to create additional directories and + * then bind mounts to other destinations. + * + * It is useful to poke unexpected holes in the read-only core snap. + **/ +static void sc_quirk_setup_tmpfs(const char *dirname) +{ + debug("mounting tmpfs at %s", dirname); + if (mount("none", dirname, "tmpfs", MS_NODEV | MS_NOSUID, NULL) != 0) { + die("cannot mount tmpfs at %s", dirname); + }; +} + +/** + * Create an empty directory and bind mount something there. + * + * The empty directory is created at destdir. The bind mount is + * done from srcdir to destdir. The bind mount is performed with + * caller-defined flags. + **/ +static void sc_quirk_mkdir_bind(const char *src_dir, const char *dest_dir, + unsigned flags) +{ + flags |= MS_BIND; + debug("creating empty directory at %s", dest_dir); + if (sc_nonfatal_mkpath(dest_dir, 0755) < 0) { + die("cannot create empty directory at %s", dest_dir); + } + char buf[1000] = { 0 }; + const char *flags_str = sc_mount_opt2str(buf, sizeof buf, flags); + debug("performing operation: mount %s %s -o %s", src_dir, dest_dir, + flags_str); + if (mount(src_dir, dest_dir, NULL, flags, NULL) != 0) { + die("cannot perform operation: mount %s %s -o %s", src_dir, + dest_dir, flags_str); + } +} + +/** + * Create a writable mimic directory based on reference directory. + * + * The mimic directory is a tmpfs populated with bind mounts to the (possibly + * read only) directories in the reference directory. While all the read-only + * content stays read-only the actual mimic directory is writable so additional + * content can be placed there. + * + * Flags are forwarded to sc_quirk_mkdir_bind() + **/ +static void sc_quirk_create_writable_mimic(const char *mimic_dir, + const char *ref_dir, unsigned flags) +{ + debug("creating writable mimic directory %s based on %s", mimic_dir, + ref_dir); + sc_quirk_setup_tmpfs(mimic_dir); + + // Now copy the ownership and permissions of the mimicked directory + struct stat stat_buf; + if (stat(ref_dir, &stat_buf) < 0) { + die("cannot stat %s", ref_dir); + } + if (chown(mimic_dir, stat_buf.st_uid, stat_buf.st_gid) < 0) { + die("cannot chown for %s", mimic_dir); + } + if (chmod(mimic_dir, stat_buf.st_mode) < 0) { + die("cannot chmod for %s", mimic_dir); + } + + debug("bind-mounting all the files from the reference directory"); + DIR *dirp SC_CLEANUP(sc_cleanup_closedir) = NULL; + dirp = opendir(ref_dir); + if (dirp == NULL) { + die("cannot open reference directory %s", ref_dir); + } + struct dirent *entryp = NULL; + do { + char src_name[PATH_MAX * 2] = { 0 }; + char dest_name[PATH_MAX * 2] = { 0 }; + // Set errno to zero, if readdir fails it will not only return null but + // set errno to a non-zero value. This is how we can differentiate + // end-of-directory from an actual error. + errno = 0; + entryp = readdir(dirp); + if (entryp == NULL && errno != 0) { + die("cannot read another directory entry"); + } + if (entryp == NULL) { + break; + } + if (strcmp(entryp->d_name, ".") == 0 + || strcmp(entryp->d_name, "..") == 0) { + continue; + } + if (entryp->d_type != DT_DIR && entryp->d_type != DT_REG) { + die("unsupported entry type of file %s (%d)", + entryp->d_name, entryp->d_type); + } + sc_must_snprintf(src_name, sizeof src_name, "%s/%s", ref_dir, + entryp->d_name); + sc_must_snprintf(dest_name, sizeof dest_name, "%s/%s", + mimic_dir, entryp->d_name); + sc_quirk_mkdir_bind(src_name, dest_name, flags); + } while (entryp != NULL); +} + +/** + * Setup a quirk for LXD. + * + * An existing LXD snap relies on pre-chroot behavior to access /var/lib/lxd + * while in devmode. Since that directory doesn't exist in the core snap the + * quirk punches a custom hole so that this directory shows the hostfs content + * if such directory exists on the host. + * + * See: https://bugs.launchpad.net/snap-confine/+bug/1613845 + **/ +static void sc_setup_lxd_quirk(void) +{ + const char *hostfs_lxd_dir = SC_HOSTFS_DIR "/var/lib/lxd"; + if (access(hostfs_lxd_dir, F_OK) == 0) { + const char *lxd_dir = "/var/lib/lxd"; + debug("setting up quirk for LXD (see LP: #1613845)"); + sc_quirk_mkdir_bind(hostfs_lxd_dir, lxd_dir, + MS_REC | MS_SLAVE | MS_NODEV | MS_NOSUID | + MS_NOEXEC); + } +} + +void sc_setup_quirks(void) +{ + // because /var/lib/snapd is essential let's move it to /tmp/snapd for a sec + char snapd_tmp[] = "/tmp/snapd.quirks_XXXXXX"; + if (mkdtemp(snapd_tmp) == 0) { + die("cannot create temporary directory for /var/lib/snapd mount point"); + } + debug("performing operation: mount --move %s %s", "/var/lib/snapd", + snapd_tmp); + if (mount("/var/lib/snapd", snapd_tmp, NULL, MS_MOVE, NULL) + != 0) { + die("cannot perform operation: mount --move %s %s", + "/var/lib/snapd", snapd_tmp); + } + // now let's make /var/lib the vanilla /var/lib from the core snap + char buf[PATH_MAX] = { 0 }; + sc_must_snprintf(buf, sizeof buf, "%s/var/lib", + sc_get_inner_core_mount_point()); + sc_quirk_create_writable_mimic("/var/lib", buf, + MS_RDONLY | MS_REC | MS_SLAVE | MS_NODEV + | MS_NOSUID); + // now let's move /var/lib/snapd (that was originally there) back + debug("performing operation: umount %s", "/var/lib/snapd"); + if (umount("/var/lib/snapd") != 0) { + die("cannot perform operation: umount %s", "/var/lib/snapd"); + } + debug("performing operation: mount --move %s %s", snapd_tmp, + "/var/lib/snapd"); + if (mount(snapd_tmp, "/var/lib/snapd", NULL, MS_MOVE, NULL) + != 0) { + die("cannot perform operation: mount --move %s %s", snapd_tmp, + "/var/lib/snapd"); + } + debug("performing operation: rmdir %s", snapd_tmp); + if (rmdir(snapd_tmp) != 0) { + die("cannot perform operation: rmdir %s", snapd_tmp); + } + // We are now ready to apply any quirks that relate to /var/lib + sc_setup_lxd_quirk(); +} diff --git a/cmd/snap-confine/quirks.h b/cmd/snap-confine/quirks.h new file mode 100644 index 00000000..f707423b --- /dev/null +++ b/cmd/snap-confine/quirks.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_QUIRKS_H +#define SNAP_QUIRKS_H + +/** + * Setup various quirks that have to exists for now. + * + * This function applies non-standard tweaks that are required + * because of requirement to stay compatible with certain snaps + * that were tested with pre-chroot layout. + **/ +void sc_setup_quirks(void); + +#endif diff --git a/cmd/snap-confine/seccomp-support.c b/cmd/snap-confine/seccomp-support.c new file mode 100644 index 00000000..76d8bde4 --- /dev/null +++ b/cmd/snap-confine/seccomp-support.c @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2015-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "config.h" +#include "seccomp-support.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/secure-getenv.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +static const char *filter_profile_dir = "/var/lib/snapd/seccomp/bpf/"; + +// MAX_BPF_SIZE is an arbitrary limit. +#define MAX_BPF_SIZE (32 * 1024) + +typedef struct sock_filter bpf_instr; + +static void validate_path_has_strict_perms(const char *path) +{ + struct stat stat_buf; + if (stat(path, &stat_buf) < 0) { + die("cannot stat %s", path); + } + + errno = 0; + if (stat_buf.st_uid != 0 || stat_buf.st_gid != 0) { + die("%s not root-owned %i:%i", path, stat_buf.st_uid, + stat_buf.st_gid); + } + + if (stat_buf.st_mode & S_IWOTH) { + die("%s has 'other' write %o", path, stat_buf.st_mode); + } +} + +static void validate_bpfpath_is_safe(const char *path) +{ + if (path == NULL || strlen(path) == 0 || path[0] != '/') { + die("valid_bpfpath_is_safe needs an absolute path as input"); + } + // strtok_r() modifies its first argument, so work on a copy + char *tokenized SC_CLEANUP(sc_cleanup_string) = NULL; + tokenized = strdup(path); + if (tokenized == NULL) { + die("cannot allocate memory for copy of path"); + } + // allocate a string large enough to hold path, and initialize it to + // '/' + size_t checked_path_size = strlen(path) + 1; + char *checked_path SC_CLEANUP(sc_cleanup_string) = NULL; + checked_path = calloc(checked_path_size, 1); + if (checked_path == NULL) { + die("cannot allocate memory for checked_path"); + } + + checked_path[0] = '/'; + checked_path[1] = '\0'; + + // validate '/' + validate_path_has_strict_perms(checked_path); + + // strtok_r needs a pointer to keep track of where it is in the + // string. + char *buf_saveptr = NULL; + + // reconstruct the path from '/' down to profile_name + char *buf_token = strtok_r(tokenized, "/", &buf_saveptr); + while (buf_token != NULL) { + char *prev SC_CLEANUP(sc_cleanup_string) = NULL; + prev = strdup(checked_path); // needed by vsnprintf in sc_must_snprintf + if (prev == NULL) { + die("cannot allocate memory for copy of checked_path"); + } + // append '' if checked_path is '/', otherwise '/' + if (strlen(checked_path) == 1) { + sc_must_snprintf(checked_path, checked_path_size, + "%s%s", prev, buf_token); + } else { + sc_must_snprintf(checked_path, checked_path_size, + "%s/%s", prev, buf_token); + } + validate_path_has_strict_perms(checked_path); + + buf_token = strtok_r(NULL, "/", &buf_saveptr); + } +} + +int sc_apply_seccomp_bpf(const char *filter_profile) +{ + debug("loading bpf program for security tag %s", filter_profile); + + char profile_path[PATH_MAX] = { 0 }; + sc_must_snprintf(profile_path, sizeof(profile_path), "%s/%s.bin", + filter_profile_dir, filter_profile); + + // Wait some time for the security profile to show up. When + // the system boots snapd will created security profiles, but + // a service snap (e.g. network-manager) starts in parallel with + // snapd so for such snaps, the profiles may not be generated + // yet + long max_wait = 120; + const char *MAX_PROFILE_WAIT = getenv("SNAP_CONFINE_MAX_PROFILE_WAIT"); + if (MAX_PROFILE_WAIT != NULL) { + char *endptr = NULL; + errno = 0; + long env_max_wait = strtol(MAX_PROFILE_WAIT, &endptr, 10); + if (errno != 0 || MAX_PROFILE_WAIT == endptr || *endptr != '\0' + || env_max_wait <= 0) { + die("SNAP_CONFINE_MAX_PROFILE_WAIT invalid"); + } + max_wait = env_max_wait > 0 ? env_max_wait : max_wait; + } + if (max_wait > 3600) { + max_wait = 3600; + } + for (long i = 0; i < max_wait; ++i) { + if (access(profile_path, F_OK) == 0) { + break; + } + sleep(1); + } + + // validate '/' down to profile_path are root-owned and not + // 'other' writable to avoid possibility of privilege + // escalation via bpf program load when paths are incorrectly + // set on the system. + validate_bpfpath_is_safe(profile_path); + + // load bpf + char bpf[MAX_BPF_SIZE + 1] = { 0 }; // account for EOF + FILE *fp = fopen(profile_path, "rb"); + if (fp == NULL) { + die("cannot read %s", profile_path); + } + // set 'size' to 1 to get bytes transferred + size_t num_read = fread(bpf, 1, sizeof(bpf), fp); + if (ferror(fp) != 0) { + die("cannot read seccomp profile %s", profile_path); + } else if (feof(fp) == 0) { + die("seccomp profile %s exceeds %zu bytes", profile_path, + sizeof(bpf)); + } + fclose(fp); + debug("read %zu bytes from %s", num_read, profile_path); + + if (sc_streq(bpf, "@unrestricted\n")) { + return 0; + } + + uid_t real_uid, effective_uid, saved_uid; + if (getresuid(&real_uid, &effective_uid, &saved_uid) < 0) { + die("cannot call getresuid"); + } + // If we can, raise privileges so that we can load the BPF into the + // kernel via 'prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)'. + debug("raising privileges to load seccomp profile"); + if (effective_uid != 0 && saved_uid == 0) { + if (seteuid(0) != 0) { + die("seteuid failed"); + } + if (geteuid() != 0) { + die("raising privs before seccomp_load did not work"); + } + } + // Load filter into the kernel. Importantly we are + // intentionally *not* setting NO_NEW_PRIVS because it + // interferes with exec transitions in AppArmor with certain + // snappy interfaces. Not setting NO_NEW_PRIVS does mean that + // applications can adjust their sandbox if they have + // CAP_SYS_ADMIN or, if running on < 4.8 kernels, break out of + // the seccomp via ptrace. Both CAP_SYS_ADMIN and 'ptrace + // (trace)' are blocked by AppArmor with typical snappy + // interfaces. + struct sock_fprog prog = { + .len = num_read / sizeof(struct sock_filter), + .filter = (struct sock_filter *)bpf, + }; + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) { + die("cannot apply seccomp profile"); + } + // drop privileges again + debug("dropping privileges after loading seccomp profile"); + if (geteuid() == 0) { + unsigned real_uid = getuid(); + if (seteuid(real_uid) != 0) { + die("seteuid failed"); + } + if (real_uid != 0 && geteuid() == 0) { + die("dropping privs after seccomp_load did not work"); + } + } + + return 0; +} diff --git a/cmd/snap-confine/seccomp-support.h b/cmd/snap-confine/seccomp-support.h new file mode 100644 index 00000000..8a0b607a --- /dev/null +++ b/cmd/snap-confine/seccomp-support.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2015-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#ifndef SNAP_CONFINE_SECCOMP_SUPPORT_H +#define SNAP_CONFINE_SECCOMP_SUPPORT_H + +#include + +/** + * Load and apply the given bpf program + * + **/ +int sc_apply_seccomp_bpf(const char *filter_profile); + +#endif diff --git a/cmd/snap-confine/snap-confine-args-test.c b/cmd/snap-confine/snap-confine-args-test.c new file mode 100644 index 00000000..05d44ad4 --- /dev/null +++ b/cmd/snap-confine/snap-confine-args-test.c @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "snap-confine-args.h" +#include "snap-confine-args.c" +#include "../libsnap-confine-private/cleanup-funcs.h" + +#include + +#include + +/** + * Create an argc + argv pair out of a NULL terminated argument list. + **/ +static void + __attribute__ ((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...) +{ + int argc = 0; + char **argv = NULL; + g_test_queue_free(argv); + + va_list ap; + va_start(ap, argvp); + const char *arg; + do { + arg = va_arg(ap, const char *); + // XXX: yeah, wrong way but the worse that can happen is for test to fail + argv = realloc(argv, sizeof(const char **) * (argc + 1)); + g_assert_nonnull(argv); + if (arg != NULL) { + char *arg_copy = strdup(arg); + g_test_queue_free(arg_copy); + argv[argc] = arg_copy; + argc += 1; + } else { + argv[argc] = NULL; + } + } while (arg != NULL); + va_end(ap); + + *argcp = argc; + *argvp = argv; +} + +static void test_test_argc_argv(void) +{ + // Check that test_argc_argv() correctly stores data + int argc; + char **argv; + + test_argc_argv(&argc, &argv, NULL); + g_assert_cmpint(argc, ==, 0); + g_assert_null(argv[0]); + + test_argc_argv(&argc, &argv, "zero", "one", "two", NULL); + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "zero"); + g_assert_cmpstr(argv[1], ==, "one"); + g_assert_cmpstr(argv[2], ==, "two"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__typical(void) +{ + // Test that typical invocation of snap-confine is parsed correctly. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "snap.SNAP_NAME.APP_NAME", + "/usr/lib/snapd/snap-exec", "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + g_assert_null(sc_args_base_snap(args)); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_cleanup_args(void) +{ + // Check that NULL argument parser can be cleaned up + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args = NULL; + sc_cleanup_args(&args); + + // Check that a non-NULL argument parser can be cleaned up + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "snap.SNAP_NAME.APP_NAME", "/usr/lib/snapd/snap-exec", + NULL); + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + sc_cleanup_args(&args); + g_assert_null(args); +} + +static void test_sc_nonfatal_parse_args__typical_classic(void) +{ + // Test that typical invocation of snap-confine is parsed correctly. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--classic", + "snap.SNAP_NAME.APP_NAME", "/usr/lib/snapd/snap-exec", + "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, true); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__ubuntu_core_launcher(void) +{ + // Test that typical legacy invocation of snap-confine via the + // ubuntu-core-launcher symlink, with duplicated security tag, is parsed + // correctly. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/bin/ubuntu-core-launcher", + "snap.SNAP_NAME.APP_NAME", "snap.SNAP_NAME.APP_NAME", + "/usr/lib/snapd/snap-exec", "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/bin/ubuntu-core-launcher"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__version(void) +{ + // Test that snap-confine --version is detected. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--version", "ignored", + "garbage", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_null(sc_args_security_tag(args)); + g_assert_null(sc_args_executable(args)); + g_assert_cmpint(sc_args_is_version_query(args), ==, true); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "ignored"); + g_assert_cmpstr(argv[2], ==, "garbage"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__evil_input(void) +{ + // Check that calling without any arguments is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + // NULL argcp/argvp attack + args = sc_nonfatal_parse_args(NULL, NULL, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argcp or argvp is NULL"); + + int argc; + char **argv; + + // NULL argv attack + argc = 0; + argv = NULL; + args = sc_nonfatal_parse_args(&argc, &argv, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argc is zero or argv is NULL"); + + // NULL argv[i] attack + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--version", "ignored", + "garbage", NULL); + argv[1] = NULL; // overwrite --version with NULL + args = sc_nonfatal_parse_args(&argc, &argv, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argument at index 1 is NULL"); +} + +static void test_sc_nonfatal_parse_args__nothing_to_parse(void) +{ + // Check that calling without any arguments is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argc is zero or argv is NULL"); +} + +static void test_sc_nonfatal_parse_args__no_security_tag(void) +{ + // Check that lack of security tag is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\napplication or hook security tag was not provided"); + + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__no_executable(void) +{ + // Check that lack of security tag is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "snap.SNAP_NAME.APP_NAME", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\nexecutable name was not provided"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__unknown_option(void) +{ + // Check that unrecognized option switch is reported as error. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "--frozbonicator", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\nunrecognized command line option: --frozbonicator"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__forwards_error(void) +{ + // Check that sc_nonfatal_parse_args() forwards errors. + if (g_test_subprocess()) { + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "--frozbonicator", NULL); + + // Call sc_nonfatal_parse_args() without an error handle + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + args = sc_nonfatal_parse_args(&argc, &argv, NULL); + (void)args; + + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("Usage: snap-confine \n" + "\nunrecognized command line option: --frozbonicator\n"); +} + +static void test_sc_nonfatal_parse_args__base_snap(void) +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--base", "base-snap", + "snap.SNAP_NAME.APP_NAME", "/usr/lib/snapd/snap-exec", + NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check the --base switch + g_assert_cmpstr(sc_args_base_snap(args), ==, "base-snap"); + // Check other arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); +} + +static void test_sc_nonfatal_parse_args__base_snap__missing_arg(void) +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--base", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\nthe --base option requires an argument"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__base_snap__twice(void) +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", + "--base", "base1", "--base", "base2", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "Usage: snap-confine \n" + "\nthe --base option can be used only once"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void __attribute__ ((constructor)) init(void) +{ + g_test_add_func("/args/test_argc_argv", test_test_argc_argv); + g_test_add_func("/args/sc_cleanup_args", test_sc_cleanup_args); + g_test_add_func("/args/sc_nonfatal_parse_args/typical", + test_sc_nonfatal_parse_args__typical); + g_test_add_func("/args/sc_nonfatal_parse_args/typical_classic", + test_sc_nonfatal_parse_args__typical_classic); + g_test_add_func("/args/sc_nonfatal_parse_args/ubuntu_core_launcher", + test_sc_nonfatal_parse_args__ubuntu_core_launcher); + g_test_add_func("/args/sc_nonfatal_parse_args/version", + test_sc_nonfatal_parse_args__version); + g_test_add_func("/args/sc_nonfatal_parse_args/nothing_to_parse", + test_sc_nonfatal_parse_args__nothing_to_parse); + g_test_add_func("/args/sc_nonfatal_parse_args/evil_input", + test_sc_nonfatal_parse_args__evil_input); + g_test_add_func("/args/sc_nonfatal_parse_args/no_security_tag", + test_sc_nonfatal_parse_args__no_security_tag); + g_test_add_func("/args/sc_nonfatal_parse_args/no_executable", + test_sc_nonfatal_parse_args__no_executable); + g_test_add_func("/args/sc_nonfatal_parse_args/unknown_option", + test_sc_nonfatal_parse_args__unknown_option); + g_test_add_func("/args/sc_nonfatal_parse_args/forwards_error", + test_sc_nonfatal_parse_args__forwards_error); + g_test_add_func("/args/sc_nonfatal_parse_args/base_snap", + test_sc_nonfatal_parse_args__base_snap); + g_test_add_func("/args/sc_nonfatal_parse_args/base_snap/missing-arg", + test_sc_nonfatal_parse_args__base_snap__missing_arg); + g_test_add_func("/args/sc_nonfatal_parse_args/base_snap/twice", + test_sc_nonfatal_parse_args__base_snap__twice); +} diff --git a/cmd/snap-confine/snap-confine-args.c b/cmd/snap-confine/snap-confine-args.c new file mode 100644 index 00000000..9e68cd6d --- /dev/null +++ b/cmd/snap-confine/snap-confine-args.c @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "snap-confine-args.h" + +#include + +#include "../libsnap-confine-private/utils.h" + +struct sc_args { + // The security tag that the application is intended to run with + char *security_tag; + // The executable that should be invoked + char *executable; + // Name of the base snap to use. + char *base_snap; + + // Flag indicating that --version was passed on command line. + bool is_version_query; + // Flag indicating that --classic was passed on command line. + bool is_classic_confinement; +}; + +struct sc_args *sc_nonfatal_parse_args(int *argcp, char ***argvp, + struct sc_error **errorp) +{ + struct sc_args *args = NULL; + struct sc_error *err = NULL; + + if (argcp == NULL || argvp == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argcp or argvp is NULL"); + goto out; + } + // Use dereferenced versions of argcp and argvp for convenience. + int argc = *argcp; + char **const argv = *argvp; + + if (argc == 0 || argv == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argc is zero or argv is NULL"); + goto out; + } + // Sanity check, look for NULL argv entries. + for (int i = 0; i < argc; ++i) { + if (argv[i] == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argument at index %d is NULL", + i); + goto out; + } + } + + args = calloc(1, sizeof *args); + if (args == NULL) { + die("cannot allocate memory for command line arguments object"); + } + // Check if we're being called through the ubuntu-core-launcher symlink. + // When this happens we want to skip the first positional argument as it is + // the security tag repeated (legacy). + bool ignore_first_tag = false; + char *basename = strrchr(argv[0], '/'); + if (basename != NULL) { + // NOTE: this is safe because we, at most, may move to the NUL byte + // that compares to an empty string. + basename += 1; + if (strcmp(basename, "ubuntu-core-launcher") == 0) { + ignore_first_tag = true; + } + } + // Parse option switches. + int optind; + for (optind = 1; optind < argc; ++optind) { + // Look at all the options switches that start with the minus sign ('-') + if (argv[optind][0] != '-') { + // On first non-switch argument break the loop. The next loop looks + // just for non-option arguments. This ensures that options and + // positional arguments cannot be mixed. + break; + } + // Handle option switches + if (strcmp(argv[optind], "--version") == 0) { + args->is_version_query = true; + // NOTE: --version short-circuits the parser to finish + goto done; + } else if (strcmp(argv[optind], "--classic") == 0) { + args->is_classic_confinement = true; + } else if (strcmp(argv[optind], "--base") == 0) { + if (optind + 1 >= argc) { + err = + sc_error_init(SC_ARGS_DOMAIN, + SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" + "the --base option requires an argument"); + goto out; + } + if (args->base_snap != NULL) { + err = + sc_error_init(SC_ARGS_DOMAIN, + SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" + "the --base option can be used only once"); + goto out; + + } + args->base_snap = strdup(argv[optind + 1]); + if (args->base_snap == NULL) { + die("cannot allocate memory for base snap name"); + } + optind += 1; + } else { + // Report unhandled option switches + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" + "unrecognized command line option: %s", + argv[optind]); + goto out; + } + } + + // Parse positional arguments. + // + // NOTE: optind is not reset, we just continue from where we left off in + // the loop above. + for (; optind < argc; ++optind) { + if (args->security_tag == NULL) { + // The first positional argument becomes the security tag. + if (ignore_first_tag) { + // Unless we are called as ubuntu-core-launcher, then we just + // swallow and ignore that security tag altogether. + ignore_first_tag = false; + continue; + } + args->security_tag = strdup(argv[optind]); + if (args->security_tag == NULL) { + die("cannot allocate memory for security tag"); + } + } else if (args->executable == NULL) { + // The second positional argument becomes the executable name. + args->executable = strdup(argv[optind]); + if (args->executable == NULL) { + die("cannot allocate memory for executable name"); + } + // No more positional arguments are required. + // Stop the parsing process. + break; + } + } + + // Verify that all mandatory positional arguments are present. + // Ensure that we have the security tag + if (args->security_tag == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" + "application or hook security tag was not provided"); + goto out; + } + // Ensure that we have the executable name + if (args->executable == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "Usage: snap-confine \n" + "\n" "executable name was not provided"); + goto out; + } + + int i; + done: + // "shift" the argument vector left, except for argv[0], to "consume" the + // arguments that were scanned / parsed correctly. + for (i = 1; optind + i < argc; ++i) { + argv[i] = argv[optind + i]; + } + argv[i] = NULL; + + // Write the updated argc back, argv is never modified. + *argcp = argc - optind; + + out: + // Don't return anything in case of an error. + if (err != NULL) { + sc_cleanup_args(&args); + } + // Forward the error and return + sc_error_forward(errorp, err); + return args; +} + +void sc_args_free(struct sc_args *args) +{ + if (args != NULL) { + free(args->security_tag); + args->security_tag = NULL; + free(args->executable); + args->executable = NULL; + free(args->base_snap); + args->base_snap = NULL; + free(args); + } +} + +void sc_cleanup_args(struct sc_args **ptr) +{ + sc_args_free(*ptr); + *ptr = NULL; +} + +bool sc_args_is_version_query(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain version query flag from NULL argument parser"); + } + return args->is_version_query; +} + +bool sc_args_is_classic_confinement(struct sc_args * args) +{ + if (args == NULL) { + die("cannot obtain classic confinement flag from NULL argument parser"); + } + return args->is_classic_confinement; +} + +const char *sc_args_security_tag(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain security tag from NULL argument parser"); + } + return args->security_tag; +} + +const char *sc_args_executable(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain executable from NULL argument parser"); + } + return args->executable; +} + +const char *sc_args_base_snap(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain base snap name from NULL argument parser"); + } + return args->base_snap; +} diff --git a/cmd/snap-confine/snap-confine-args.h b/cmd/snap-confine/snap-confine-args.h new file mode 100644 index 00000000..00a507c6 --- /dev/null +++ b/cmd/snap-confine/snap-confine-args.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SC_SNAP_CONFINE_ARGS_H +#define SC_SNAP_CONFINE_ARGS_H + +#include + +#include "../libsnap-confine-private/error.h" + +/** + * Error domain for errors related to argument parsing. + **/ +#define SC_ARGS_DOMAIN "args" + +enum { + /** + * Error indicating that the command line arguments could not be parsed + * correctly and usage message should be displayed to the user. + **/ + SC_ARGS_ERR_USAGE = 1, +}; + +/** + * Opaque structure describing command-line arguments to snap-confine. + **/ +struct sc_args; + +/** + * Parse command line arguments for snap-confine. + * + * Snap confine understands very specific arguments. + * + * The argument vector can begin with "ubuntu-core-launcher" (with an optional + * path) which implies that the first arctual argument (argv[1]) is a copy of + * argv[2] and can be discarded. + * + * The argument vector is scanned, left to right, to look for switches that + * start with the minus sign ('-'). Recognized options are stored and + * memorized. Unrecognized options return an appropriate error object. + * + * Currently only one option is understood, that is "--version". It is simply + * scanned, memorized and discarded. The presence of this switch can be + * retrieved with sc_args_is_version_query(). + * + * After all the option switches are scanned it is expected to scan two more + * arguments: the security tag and the name of the executable to run. An error + * object is returned when those is missing. + * + * Both argc and argv are modified so the caller can look at the first unparsed + * argument at argc[0]. This is only done if argument parsing is successful. + **/ +__attribute__ ((warn_unused_result)) +struct sc_args *sc_nonfatal_parse_args(int *argcp, char ***argvp, + struct sc_error **errorp); + +/** + * Free the object describing command-line arguments to snap-confine. + **/ +void sc_args_free(struct sc_args *args); + +/** + * Cleanup an error with sc_args_free() + * + * This function is designed to be used with + * SC_CLEANUP(sc_cleanup_args). + **/ +void sc_cleanup_args(struct sc_args **ptr); + +/** + * Check if snap-confine was invoked with the --version switch. + **/ +bool sc_args_is_version_query(struct sc_args *args); + +/** + * Check if snap-confine was invoked with the --classic switch. + **/ +bool sc_args_is_classic_confinement(struct sc_args *args); + +/** + * Get the security tag passed to snap-confine. + * + * The return value may be NULL if snap-confine was invoked with --version. It + * is never NULL otherwise. + * + * The return value must not be freed(). It is bound to the lifetime of + * the argument parser. + **/ +const char *sc_args_security_tag(struct sc_args *args); + +/** + * Get the executable name passed to snap-confine. + * + * The return value may be NULL if snap-confine was invoked with --version. It + * is never NULL otherwise. + * + * The return value must not be freed(). It is bound to the lifetime of + * the argument parser. + **/ +const char *sc_args_executable(struct sc_args *args); + +/** + * Get the name of the base snap to use. + * + * The return value must not be freed(). It is bound to the lifetime of + * the argument parser. + **/ +const char *sc_args_base_snap(struct sc_args *args); + +#endif diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in new file mode 100644 index 00000000..6f0e9280 --- /dev/null +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -0,0 +1,559 @@ +# Author: Jamie Strandboge +#include + +@LIBEXECDIR@/snap-confine (attach_disconnected) { + # Include any additional files that snapd chose to generate. + # - for $HOME on NFS + # - for $HOME on encrypted media + # + # Those are discussed on https://forum.snapcraft.io/t/snapd-vs-upstream-kernel-vs-apparmor + # and https://forum.snapcraft.io/t/snaps-and-nfs-home/ + #include "/var/lib/snapd/apparmor/snap-confine" + + # We run privileged, so be fanatical about what we include and don't use + # any abstractions + /etc/ld.so.cache r, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}ld-*.so mrix, + # libc, you are funny + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libc{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpthread{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libreadline{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}librt{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libgcc_s.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libncursesw{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libresolv{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libselinux.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpcre.so* mr, + # normal libs in order + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libapparmor.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcgmanager.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libdl{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnih.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnih-dbus.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libdbus-1.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libudev.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libseccomp.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcap.so* mr, + + @LIBEXECDIR@/snap-confine mr, + + /dev/null rw, + /dev/full rw, + /dev/zero rw, + /dev/random r, + /dev/urandom r, + /dev/pts/[0-9]* rw, + /dev/tty rw, + + # cgroup: devices + capability sys_admin, + capability dac_override, + /sys/fs/cgroup/devices/snap{,py}.*/ w, + /sys/fs/cgroup/devices/snap{,py}.*/tasks w, + /sys/fs/cgroup/devices/snap{,py}.*/devices.{allow,deny} w, + + # cgroup: freezer + # Allow creating per-snap cgroup freezers and adding snap command (task) + # invocations to the freezer. This allows for reliably enumerating all + # running tasks for the snap. + /sys/fs/cgroup/freezer/ r, + /sys/fs/cgroup/freezer/snap.*/ w, + /sys/fs/cgroup/freezer/snap.*/tasks w, + + # querying udev + /etc/udev/udev.conf r, + /sys/**/uevent r, + /lib/udev/snappy-app-dev ixr, # drop + /run/udev/** rw, + /{,usr/}bin/tr ixr, + /usr/lib/locale/** r, + /usr/lib/@{multiarch}/gconv/gconv-modules r, + /usr/lib/@{multiarch}/gconv/gconv-modules.cache r, + + # priv dropping + capability setuid, + capability setgid, + + # changing profile + @{PROC}/[0-9]*/attr/exec w, + # Reading current profile + @{PROC}/[0-9]*/attr/current r, + # Reading available filesystems + @{PROC}/filesystems r, + + # To find where apparmor is mounted + @{PROC}/[0-9]*/mounts r, + # To find if apparmor is enabled + /sys/module/apparmor/parameters/enabled r, + + # Don't allow changing profile to unconfined or profiles that start with + # '/'. Use 'unsafe' to support snap-exec on armhf and its reliance on + # the environment for determining the capabilities of the architecture. + # 'unsafe' is ok here because the kernel will have already cleared the + # environment as part of launching snap-confine with + # CAP_SYS_ADMIN. + change_profile unsafe /** -> [^u/]**, + change_profile unsafe /** -> u[^n]**, + change_profile unsafe /** -> un[^c]**, + change_profile unsafe /** -> unc[^o]**, + change_profile unsafe /** -> unco[^n]**, + change_profile unsafe /** -> uncon[^f]**, + change_profile unsafe /** -> unconf[^i]**, + change_profile unsafe /** -> unconfi[^n]**, + change_profile unsafe /** -> unconfin[^e]**, + change_profile unsafe /** -> unconfine[^d]**, + change_profile unsafe /** -> unconfined?**, + + # allow changing to a few not caught above + change_profile unsafe /** -> {u,un,unc,unco,uncon,unconf,unconfi,unconfin,unconfine}, + + # LP: #1446794 - when this bug is fixed, change the above to: + # deny change_profile unsafe /** -> {unconfined,/**}, + # change_profile unsafe /** -> **, + + # reading seccomp filters + /{tmp/snap.rootfs_*/,}var/lib/snapd/seccomp/bpf/*.bin r, + + # ensuring correct permissions in sc_quirk_create_writable_mimic + /{tmp/snap.rootfs_*/,}var/lib/ rw, + + # LP: #1668659 + mount options=(rw rbind) /snap/ -> /snap/, + mount options=(rw rshared) -> /snap/, + + # boostrapping the mount namespace + mount options=(rw rshared) -> /, + mount options=(rw bind) /tmp/snap.rootfs_*/ -> /tmp/snap.rootfs_*/, + mount options=(rw unbindable) -> /tmp/snap.rootfs_*/, + # the next line is for classic system + mount options=(rw rbind) @SNAP_MOUNT_DIR@/*/*/ -> /tmp/snap.rootfs_*/, + # the next line is for core system + mount options=(rw rbind) / -> /tmp/snap.rootfs_*/, + # all of the constructed rootfs is a rslave + mount options=(rw rslave) -> /tmp/snap.rootfs_*/, + # bidirectional mounts (for both classic and core) + # NOTE: this doesn't capture the MERGED_USR configuration option so that + # when a distro with merged /usr and / that uses apparmor shows up it + # should be handled here. + /{,run/}media/ w, + mount options=(rw rbind) /media/ -> /tmp/snap.rootfs_*/media/, + /run/netns/ w, + mount options=(rw rbind) /run/netns/ -> /tmp/snap.rootfs_*/run/netns/, + # unidirectional mounts (only for classic system) + mount options=(rw rbind) /dev/ -> /tmp/snap.rootfs_*/dev/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/dev/, + + mount options=(rw rbind) /etc/ -> /tmp/snap.rootfs_*/etc/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/etc/, + + mount options=(rw rbind) /home/ -> /tmp/snap.rootfs_*/home/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/home/, + + mount options=(rw rbind) /root/ -> /tmp/snap.rootfs_*/root/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/root/, + + mount options=(rw rbind) /proc/ -> /tmp/snap.rootfs_*/proc/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/proc/, + + mount options=(rw rbind) /sys/ -> /tmp/snap.rootfs_*/sys/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/sys/, + + mount options=(rw rbind) /tmp/ -> /tmp/snap.rootfs_*/tmp/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/tmp/, + + mount options=(rw rbind) /var/lib/snapd/ -> /tmp/snap.rootfs_*/var/lib/snapd/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/lib/snapd/, + + mount options=(rw rbind) /var/snap/ -> /tmp/snap.rootfs_*/var/snap/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/snap/, + + mount options=(rw rbind) /var/tmp/ -> /tmp/snap.rootfs_*/var/tmp/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/tmp/, + + mount options=(rw rbind) /run/ -> /tmp/snap.rootfs_*/run/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/run/, + + mount options=(rw rbind) {,/usr}/lib{,32,64,x32}/modules/ -> /tmp/snap.rootfs_*{,/usr}/lib/modules/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*{,/usr}/lib/modules/, + + mount options=(rw rbind) /var/log/ -> /tmp/snap.rootfs_*/var/log/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/log/, + + mount options=(rw rbind) /usr/src/ -> /tmp/snap.rootfs_*/usr/src/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/usr/src/, + + # allow making host snap-exec available inside base snaps + mount options=(rw bind) @LIBEXECDIR@/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/usr/lib/snapd/, + + # allow making re-execed host snap-exec available inside base snaps + mount options=(ro bind) @SNAP_MOUNT_DIR@/core/*/usr/lib/snapd/ -> /tmp/snap.rootfs_*/usr/lib/snapd/, + + mount options=(rw bind) /usr/bin/snapctl -> /tmp/snap.rootfs_*/usr/bin/snapctl, + mount options=(rw slave) -> /tmp/snap.rootfs_*/usr/bin/snapctl, + + # /etc/alternatives (classic) + mount options=(rw bind) @SNAP_MOUNT_DIR@/{,ubuntu-}core/*/etc/alternatives/ -> /tmp/snap.rootfs_*/etc/alternatives/, + mount options=(rw bind) @SNAP_MOUNT_DIR@/*/*/etc/ssl/ -> /tmp/snap.rootfs_*/etc/ssl/, + mount options=(rw bind) @SNAP_MOUNT_DIR@/*/*/etc/nsswitch.conf -> /tmp/snap.rootfs_*/etc/nsswitch.conf, + # /etc/alternatives (core) + mount options=(rw bind) /etc/alternatives/ -> /tmp/snap.rootfs_*/etc/alternatives/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/etc/alternatives/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/etc/ssl/, + mount options=(rw slave) -> /tmp/snap.rootfs_*/etc/nsswitch.conf, + # the /snap directory + mount options=(rw rbind) @SNAP_MOUNT_DIR@/ -> /tmp/snap.rootfs_*/snap/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/snap/, + # pivot_root preparation and execution + mount options=(rw bind) /tmp/snap.rootfs_*/var/lib/snapd/hostfs/ -> /tmp/snap.rootfs_*/var/lib/snapd/hostfs/, + mount options=(rw private) -> /tmp/snap.rootfs_*/var/lib/snapd/hostfs/, + pivot_root, + # cleanup + umount /var/lib/snapd/hostfs/tmp/snap.rootfs_*/, + umount /var/lib/snapd/hostfs/sys/, + umount /var/lib/snapd/hostfs/dev/, + umount /var/lib/snapd/hostfs/proc/, + mount options=(rw rslave) -> /var/lib/snapd/hostfs/, + + # Allow reading the os-release file (possibly a symlink to /usr/lib). + /{etc/,usr/lib/}os-release r, + + # set up snap-specific private /tmp dir + capability chown, + /tmp/ w, + /tmp/snap.*/ w, + /tmp/snap.*/tmp/ w, + mount options=(rw private) -> /tmp/, + mount options=(rw bind) /tmp/snap.*/tmp/ -> /tmp/, + mount fstype=devpts options=(rw) devpts -> /dev/pts/, + mount options=(rw bind) /dev/pts/ptmx -> /dev/ptmx, # for bind mounting + mount options=(rw bind) /dev/pts/ptmx -> /dev/pts/ptmx, # for bind mounting under LXD + # Workaround for LP: #1584456 on older kernels that mistakenly think + # /dev/pts/ptmx needs a trailing '/' + mount options=(rw bind) /dev/pts/ptmx/ -> /dev/ptmx/, + mount options=(rw bind) /dev/pts/ptmx/ -> /dev/pts/ptmx/, + + # for running snaps on classic + /snap/ r, + /snap/** r, + @SNAP_MOUNT_DIR@/ r, + @SNAP_MOUNT_DIR@/** r, + + # NOTE: at this stage the /snap directory is stable as we have called + # pivot_root already. + + # nvidia handling, glob needs /usr/** and the launcher must be + # able to bind mount the nvidia dir + /sys/module/nvidia/version r, + /sys/**/drivers/nvidia{,_*}/* r, + /sys/**/nvidia*/uevent r, + /sys/module/nvidia{,_*}/* r, + /dev/nvidia[0-9]* r, + /dev/nvidiactl r, + /dev/nvidia-uvm r, + /usr/** r, + mount options=(rw bind) /usr/lib{,32}/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl{,32}/, + mount options=(rw bind) /usr/lib{,32}/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl{,32}/, + /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/* w, + mount fstype=tmpfs options=(rw nodev noexec) none -> /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/, + mount options=(remount ro) -> /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/, + + # Vulkan support + /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/* w, + mount fstype=tmpfs options=(rw nodev noexec) none -> /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/, + mount options=(remount ro) -> /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/, + + # create gl dirs as needed + /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/ w, + /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/ w, + + # for chroot on steroids, we use pivot_root as a better chroot that makes + # apparmor rules behave the same on classic and outside of classic. + + # for creating the user data directories: ~/snap, ~/snap/ and + # ~/snap// + / r, + @{HOMEDIRS}/ r, + # These should both have 'owner' match but due to LP: #1466234, we can't + # yet + @{HOME}/ r, + @{HOME}/snap/{,*/,*/*/} rw, + + # for creating the user shared memory directories + /{dev,run}/{,shm/} r, + # This should both have 'owner' match but due to LP: #1466234, we can't yet + /{dev,run}/shm/{,*/,*/*/} rw, + + # for creating the user XDG_RUNTIME_DIR: /run/user, /run/user/UID and + # /run/user/UID/ + /run/user/{,[0-9]*/,[0-9]*/*/} rw, + + # Workaround https://launchpad.net/bugs/359338 until upstream handles + # stacked filesystems generally. + # encrypted ~/.Private and old-style encrypted $HOME + @{HOME}/.Private/ r, + @{HOME}/.Private/** mrixwlk, + # new-style encrypted $HOME + @{HOMEDIRS}/.ecryptfs/*/.Private/ r, + @{HOMEDIRS}/.ecryptfs/*/.Private/** mrixwlk, + + # Allow snap-confine to move to the void + /var/lib/snapd/void/ r, + + # Allow snap-confine to read snap contexts + /var/lib/snapd/context/snap.* r, + + # Support for the quirk system + /var/ r, + /var/lib/ r, + /var/lib/** rw, + /tmp/ r, + /tmp/snapd.quirks_*/ rw, + mount options=(move) /var/lib/snapd/ -> /tmp/snapd.quirks_*/, + mount fstype=tmpfs options=(rw nodev nosuid) none -> /var/lib/, + mount options=(ro rbind) /snap/{,ubuntu-}core/*/var/lib/** -> /var/lib/**, + umount /var/lib/snapd/, + mount options=(move) /tmp/snapd.quirks_*/ -> /var/lib/snapd/, + # On classic systems with a setuid root snap-confine when run by non-root + # user, the mimic_dir is created with the gid of the calling user (ie, + # not '0') so when setting the permissions (chmod) of the mimicked + # directory to that of the reference directory, a CAP_FSETID is triggered. + # snap-confine sets the directory up correctly, so simply silence the + # denial since we don't want to grant the capability as a whole to + # snap-confine. + deny capability fsetid, + + # support for the LXD quirk + mount options=(rw rbind nodev nosuid noexec) /var/lib/snapd/hostfs/var/lib/lxd/ -> /var/lib/lxd/, + /var/lib/lxd/ w, + /var/lib/snapd/hostfs/var/lib/lxd r, + + # support for locking + /run/snapd/lock/ rw, + /run/snapd/lock/*.lock rwk, + + # support for the mount namespace sharing + capability sys_ptrace, + # allow snap-confine to read /proc/1/ns/mnt + ptrace trace peer=unconfined, + + mount options=(rw rbind) /run/snapd/ns/ -> /run/snapd/ns/, + mount options=(private) -> /run/snapd/ns/, + / rw, + /run/ rw, + /run/snapd/ rw, + /run/snapd/ns/ rw, + /run/snapd/ns/*.lock rwk, + /run/snapd/ns/*.mnt rw, + ptrace (read, readby, tracedby) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + @{PROC}/*/mountinfo r, + capability sys_chroot, + capability sys_admin, + signal (send, receive) set=(abrt) peer=@LIBEXECDIR@/snap-confine, + signal (send) set=(int) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + signal (send, receive) set=(alrm, exists) peer=@LIBEXECDIR@/snap-confine, + signal (receive) set=(exists) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + + # workaround for linux 4.13/upstream, see + # https://forum.snapcraft.io/t/snapd-2-27-6-2-in-debian-sid-blocked-on-apparmor-in-kernel-4-13-0-1/2813/3 + ptrace (trace, tracedby) peer=@LIBEXECDIR@/snapd/snap-confine, + + # For aa_change_hat() to go into ^mount-namespace-capture-helper + @{PROC}/[0-9]*/attr/current w, + + ^mount-namespace-capture-helper (attach_disconnected) { + # We run privileged, so be fanatical about what we include and don't use + # any abstractions + /etc/ld.so.cache r, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}ld-*.so mrix, + # libc, you are funny + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libc{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpthread{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libreadline{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}librt{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libgcc_s.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libncursesw{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libresolv{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libselinux.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpcre.so* mr, + # normal libs in order + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libapparmor.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcgmanager.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libdl{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnih.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnih-dbus.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libdbus-1.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libudev.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libseccomp.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcap.so* mr, + + @LIBEXECDIR@/snap-confine mr, + + /dev/null rw, + /dev/full rw, + /dev/zero rw, + /dev/random r, + /dev/urandom r, + + capability sys_ptrace, + capability sys_admin, + # This allows us to read and bind mount the namespace file + / r, + @{PROC}/ r, + @{PROC}/*/ r, + @{PROC}/*/ns/ r, + @{PROC}/*/ns/mnt r, + /run/ r, + /run/snapd/ r, + /run/snapd/ns/ r, + /run/snapd/ns/*.mnt rw, + # NOTE: the source name is / even though we map /proc/123/ns/mnt + mount options=(rw bind) / -> /run/snapd/ns/*.mnt, + # This is the SIGALRM that we send and receive if a timeout expires + signal (send, receive) set=(alrm) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + # Those two rules are exactly the same but we don't know if the parent process is still alive + # and hence has the appropriate label or is already dead and hence has no label. + signal (send) set=(exists) peer=@LIBEXECDIR@/snap-confine, + signal (send) set=(exists) peer=unconfined, + # This is so that we can abort + signal (send, receive) set=(abrt) peer=@LIBEXECDIR@/snap-confine//mount-namespace-capture-helper, + # This is the signal we get if snap-confine dies (we subscribe to it with prctl) + signal (receive) set=(int) peer=@LIBEXECDIR@/snap-confine, + # This allows snap-confine to be killed from the outside. + signal (receive) peer=unconfined, + # This allows snap-confine to wait for us + ptrace (read, trace, tracedby) peer=@LIBEXECDIR@/snap-confine, + } + + # Allow snap-confine to be killed + signal (receive) peer=unconfined, + + # Allow executing snap-update-ns when... + + # ...snap-confine is, conceptually, re-executing and uses snap-update-ns + # from the distribution package. This is also the location used when using + # the core/base snap on all-snap systems. The variants here represent + # various locations of libexecdir across distributions. + /usr/lib{,exec,64}/snapd/snap-update-ns Cxr -> snap_update_ns, + + # ...snap-confine is not, conceptually, re-executing and uses + # snap-update-ns from the distribution package but we are already inside + # the constructed mount namespace so we must traverse "hostfs". The + # variants here represent various locations of libexecdir across + # distributions. + /var/lib/snapd/hostfs/usr/lib{,exec,64}/snapd/snap-update-ns Cxr -> snap_update_ns, + + # ..snap-confine is, conceptually, re-executing and uses snap-update-ns + # from the core snap. Note that the location of the core snap varies from + # distribution to distribution. The variants here represent different + # locations of snap mount directory across distributions. + /{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-update-ns Cxr -> snap_update_ns, + + # ...snap-confine is, conceptually, re-executing and uses snap-update-ns + # from the core snap but we are already inside the constructed mount + # namespace. Here the apparmor kernel module re-constructs the path to + # snap-update-ns using the "hostfs" mount entry rather than the more + # "natural" /snap mount entry but we have no control over that. This is + # reported as (LP: #1716339). The variants here represent different + # locations of snap mount directory across distributions. + /var/lib/snapd/hostfs/{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-update-ns Cxr -> snap_update_ns, + + profile snap_update_ns (attach_disconnected) { + # The next four rules mirror those above. We want to be able to read + # and map snap-update-ns into memory but it may come from a variety of places. + /usr/lib{,exec,64}/snapd/snap-update-ns mr, + /var/lib/snapd/hostfs/usr/lib{,exec,64}/snapd/snap-update-ns mr, + /{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-update-ns mr, + /var/lib/snapd/hostfs/{,var/lib/snapd/}snap/core/*/usr/lib/snapd/snap-update-ns mr, + + # Allow reading the dynamic linker cache. + /etc/ld.so.cache r, + # Allow reading, mapping and executing the dynamic linker. + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}ld-*.so mrix, + # Allow reading and mapping various parts of the standard library and + # dynamically loaded nss modules and what not. + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libc{,-[0-9]*}.so* mr, + /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpthread{,-[0-9]*}.so* mr, + + # Allow reading the command line (snap-update-ns uses it in pre-Go bootstrap code). + @{PROC}/@{pid}/cmdline r, + + # Allow reading the os-release file (possibly a symlink to /usr/lib). + /{etc/,usr/lib/}os-release r, + + # Allow creating/grabbing various snapd lock files. + /run/snapd/lock/*.lock rwk, + + # Allow reading stored mount namespaces, + /run/snapd/ns/ r, + /run/snapd/ns/*.mnt r, + + # Allow reading per-snap desired mount profiles. Those are written by + # snapd and represent the desired layout and content connections. + /var/lib/snapd/mount/snap.*.fstab r, + + # Allow reading and writing actual per-snap mount profiles. Note that + # the second rule is generic to allow our tmpfile-rename approach to + # writing them. Those are written by snap-update-ns and represent the + # actual layout at a given moment. + /run/snapd/ns/*.fstab rw, + /run/snapd/ns/*.fstab.* rw, + + # NOTE: at this stage the /snap directory is stable as we have called + # pivot_root already. + + # Needed to perform mount/unmounts. + capability sys_admin, + + # Allow freezing and thawing the per-snap cgroup freezers + /sys/fs/cgroup/freezer/snap.*/freezer.state rw, + + # Support mount profiles via the content interface. This should correspond + # to permutations of $SNAP -> $SNAP for reading and $SNAP_{DATA,COMMON} -> + # $SNAP_{DATA,COMMON} for both reading and writing. + # + # Note that: + # /snap/*/*/** + # is meant to mean: + # /snap/$SNAP_NAME/$SNAP_REVISION/and-any-subdirectory + # but: + # /var/snap/*/** + # is meant to mean: + # /var/snap/$SNAP_NAME/$SNAP_REVISION/ + mount options=(ro bind) /snap/*/** -> /snap/*/*/**, + mount options=(ro bind) /snap/*/** -> /var/snap/*/**, + mount options=(rw bind) /var/snap/*/** -> /var/snap/*/**, + mount options=(ro bind) /var/snap/*/** -> /var/snap/*/**, + + # Allow creating missing mount directories under $SNAP_DATA. + # + # The "tree" of permissions is needed for SecureMkdirAll that uses + # open(..., O_NOFOLLOW) and mkdirat() using the resulting file + # descriptor. + / r, + /var/ r, + /var/snap/{,*/} r, + /var/snap/*/**/ rw, + + # Allow the content interface to bind fonts from the host filesystem + mount options=(ro bind) /var/lib/snapd/hostfs/usr/share/fonts/ -> /snap/*/*/**, + # Allow the desktop interface to bind fonts from the host filesystem + mount options=(ro bind) /var/lib/snapd/hostfs/usr/share/fonts/ -> /usr/share/fonts/, + mount options=(ro bind) /var/lib/snapd/hostfs/usr/local/share/fonts/ -> /usr/local/share/fonts/, + mount options=(ro bind) /var/lib/snapd/hostfs/var/cache/fontconfig/ -> /var/cache/fontconfig/, + + # Allow unmounts matching possible mounts listed above. + umount /snap/*/*/**, + umount /var/snap/*/**, + umount /usr/share/fonts, + umount /usr/local/share/fonts, + umount /var/cache/fontconfig, + + # But we don't want anyone to touch /snap/bin + audit deny mount /snap/bin/** -> /**, + audit deny mount /** -> /snap/bin/**, + + # Allow the content interface to bind fonts from the host filesystem + mount options=(ro bind) /var/lib/snapd/hostfs/usr/share/fonts/ -> /snap/*/*/**, + } +} diff --git a/cmd/snap-confine/snap-confine.c b/cmd/snap-confine/snap-confine.c new file mode 100644 index 00000000..7b8693a7 --- /dev/null +++ b/cmd/snap-confine/snap-confine.c @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/cgroup-freezer-support.h" +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/locking.h" +#include "../libsnap-confine-private/secure-getenv.h" +#include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/utils.h" +#include "apparmor-support.h" +#include "mount-support.h" +#include "ns-support.h" +#include "quirks.h" +#include "udev-support.h" +#include "user-support.h" +#include "cookie-support.h" +#include "snap-confine-args.h" +#ifdef HAVE_SECCOMP +#include "seccomp-support.h" +#endif // ifdef HAVE_SECCOMP + +// sc_maybe_fixup_permissions fixes incorrect permissions +// inside the mount namespace for /var/lib. Before 1ccce4 +// this directory was created with permissions 1777. +static void sc_maybe_fixup_permissions(void) +{ + struct stat buf; + if (stat("/var/lib", &buf) != 0) { + die("cannot stat /var/lib"); + } + if ((buf.st_mode & 0777) == 0777) { + if (chmod("/var/lib", 0755) != 0) { + die("cannot chmod /var/lib"); + } + if (chown("/var/lib", 0, 0) != 0) { + die("cannot chown /var/lib"); + } + } +} + +// sc_maybe_fixup_udev will remove incorrectly created udev tags +// that cause libudev on 16.04 to fail with "udev_enumerate_scan failed". +// See also: +// https://forum.snapcraft.io/t/weird-udev-enumerate-error/2360/17 +static void sc_maybe_fixup_udev(void) +{ + glob_t glob_res SC_CLEANUP(globfree) = { + .gl_pathv = NULL,.gl_pathc = 0,.gl_offs = 0,}; + const char *glob_pattern = "/run/udev/tags/snap_*/*nvidia*"; + int err = glob(glob_pattern, 0, NULL, &glob_res); + if (err == GLOB_NOMATCH) { + return; + } + if (err != 0) { + die("cannot search using glob pattern %s: %d", + glob_pattern, err); + } + // kill bogus udev tags for nvidia. They confuse udev, this + // undoes the damage from github.com/snapcore/snapd/pull/3671. + // + // The udev tagging of nvidia got reverted in: + // https://github.com/snapcore/snapd/pull/4022 + // but leftover files need to get removed or apps won't start + for (size_t i = 0; i < glob_res.gl_pathc; ++i) { + unlink(glob_res.gl_pathv[i]); + } +} + +int main(int argc, char **argv) +{ + // Use our super-defensive parser to figure out what we've been asked to do. + struct sc_error *err = NULL; + struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; + args = sc_nonfatal_parse_args(&argc, &argv, &err); + sc_die_on_error(err); + + // We've been asked to print the version string so let's just do that. + if (sc_args_is_version_query(args)) { + printf("%s %s\n", PACKAGE, PACKAGE_VERSION); + return 0; + } + + const char *snap_name = getenv("SNAP_NAME"); + if (snap_name == NULL) { + die("SNAP_NAME is not set"); + } + sc_snap_name_validate(snap_name, NULL); + + // Collect and validate the security tag and a few other things passed on + // command line. + const char *security_tag = sc_args_security_tag(args); + if (!verify_security_tag(security_tag, snap_name)) { + die("security tag %s not allowed", security_tag); + } + const char *executable = sc_args_executable(args); + const char *base_snap_name = sc_args_base_snap(args) ? : "core"; + bool classic_confinement = sc_args_is_classic_confinement(args); + + sc_snap_name_validate(base_snap_name, NULL); + + debug("security tag: %s", security_tag); + debug("executable: %s", executable); + debug("confinement: %s", + classic_confinement ? "classic" : "non-classic"); + debug("base snap: %s", base_snap_name); + + // Who are we? + uid_t real_uid, effective_uid, saved_uid; + gid_t real_gid, effective_gid, saved_gid; + getresuid(&real_uid, &effective_uid, &saved_uid); + getresgid(&real_gid, &effective_gid, &saved_gid); + debug("ruid: %d, euid: %d, suid: %d", + real_uid, effective_uid, saved_uid); + debug("rgid: %d, egid: %d, sgid: %d", + real_gid, effective_gid, saved_gid); + + // snap-confine runs as both setuid root and setgid root. + // Temporarily drop group privileges here and reraise later + // as needed. + if (effective_gid == 0 && real_gid != 0) { + if (setegid(real_gid) != 0) { + die("cannot set effective group id to %d", real_gid); + } + } +#ifndef CAPS_OVER_SETUID + // this code always needs to run as root for the cgroup/udev setup, + // however for the tests we allow it to run as non-root + if (geteuid() != 0 && secure_getenv("SNAP_CONFINE_NO_ROOT") == NULL) { + die("need to run as root or suid"); + } +#endif + + char *snap_context SC_CLEANUP(sc_cleanup_string) = NULL; + // Do no get snap context value if running a hook (we don't want to overwrite hook's SNAP_COOKIE) + if (!sc_is_hook_security_tag(security_tag)) { + struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL; + snap_context = sc_cookie_get_from_snapd(snap_name, &err); + if (err != NULL) { + error("%s\n", sc_error_msg(err)); + } + } + + struct sc_apparmor apparmor; + sc_init_apparmor_support(&apparmor); + if (!apparmor.is_confined && apparmor.mode != SC_AA_NOT_APPLICABLE + && getuid() != 0 && geteuid() == 0) { + // Refuse to run when this process is running unconfined on a system + // that supports AppArmor when the effective uid is root and the real + // id is non-root. This protects against, for example, unprivileged + // users trying to leverage the snap-confine in the core snap to + // escalate privileges. + die("snap-confine has elevated permissions and is not confined" + " but should be. Refusing to continue to avoid" + " permission escalation attacks"); + } + // TODO: check for similar situation and linux capabilities. + if (geteuid() == 0) { + if (classic_confinement) { + /* 'classic confinement' is designed to run without the sandbox + * inside the shared namespace. Specifically: + * - snap-confine skips using the snap-specific mount namespace + * - snap-confine skips using device cgroups + * - snapd sets up a lenient AppArmor profile for snap-confine to use + * - snapd sets up a lenient seccomp profile for snap-confine to use + */ + debug + ("skipping sandbox setup, classic confinement in use"); + } else { + /* snap-confine uses privately-shared /run/snapd/ns to store + * bind-mounted mount namespaces of each snap. In the case that + * snap-confine is invoked from the mount namespace it typically + * constructs, the said directory does not contain mount entries + * for preserved namespaces as those are only visible in the main, + * outer namespace. + * + * In order to operate in such an environment snap-confine must + * first re-associate its own process with another namespace in + * which the /run/snapd/ns directory is visible. The most obvious + * candidate is pid one, which definitely doesn't run in a + * snap-specific namespace, has a predictable PID and is long + * lived. + */ + sc_reassociate_with_pid1_mount_ns(); + // Do global initialization: + int global_lock_fd = sc_lock_global(); + // ensure that "/" or "/snap" is mounted with the + // "shared" option, see LP:#1668659 + debug("ensuring that snap mount directory is shared"); + sc_ensure_shared_snap_mount(); + debug("unsharing snap namespace directory"); + sc_initialize_ns_groups(); + sc_unlock_global(global_lock_fd); + + // Do per-snap initialization. + int snap_lock_fd = sc_lock(snap_name); + debug("initializing mount namespace: %s", snap_name); + struct sc_ns_group *group = NULL; + group = sc_open_ns_group(snap_name, 0); + sc_create_or_join_ns_group(group, &apparmor, + base_snap_name, snap_name); + if (sc_should_populate_ns_group(group)) { + sc_populate_mount_ns(base_snap_name, snap_name); + sc_preserve_populated_ns_group(group); + } + sc_close_ns_group(group); + // older versions of snap-confine created incorrect + // 777 permissions for /var/lib and we need to fixup + // for systems that had their NS created with an + // old version + sc_maybe_fixup_permissions(); + sc_maybe_fixup_udev(); + + // Associate each snap process with a dedicated snap freezer + // control group. This simplifies testing if any processes + // belonging to a given snap are still alive. + // See the documentation of the function for details. + + if (getegid() != 0 && saved_gid == 0) { + // Temporarily raise egid so we can chown the freezer cgroup + // under LXD. + if (setegid(0) != 0) { + die("cannot set effective group id to root"); + } + } + sc_cgroup_freezer_join(snap_name, getpid()); + if (geteuid() == 0 && real_gid != 0) { + if (setegid(real_gid) != 0) { + die("cannot set effective group id to %d", real_gid); + } + } + + sc_unlock(snap_name, snap_lock_fd); + + // Reset path as we cannot rely on the path from the host OS to + // make sense. The classic distribution may use any PATH that makes + // sense but we cannot assume it makes sense for the core snap + // layout. Note that the /usr/local directories are explicitly + // left out as they are not part of the core snap. + debug + ("resetting PATH to values in sync with core snap"); + setenv("PATH", + "/usr/local/sbin:" + "/usr/local/bin:" + "/usr/sbin:" + "/usr/bin:" + "/sbin:" + "/bin:" "/usr/games:" "/usr/local/games", 1); + // Ensure we set the various TMPDIRs to /tmp. + // One of the parts of setting up the mount namespace is to create a private /tmp + // directory (this is done in sc_populate_mount_ns() above). The host environment + // may point to a directory not accessible by snaps so we need to reset it here. + const char *tmpd[] = { "TMPDIR", "TEMPDIR", NULL }; + int i; + for (i = 0; tmpd[i] != NULL; i++) { + if (setenv(tmpd[i], "/tmp", 1) != 0) { + die("cannot set environment variable '%s'", tmpd[i]); + } + } + struct snappy_udev udev_s; + if (snappy_udev_init(security_tag, &udev_s) == 0) + setup_devices_cgroup(security_tag, &udev_s); + snappy_udev_cleanup(&udev_s); + } + // The rest does not so temporarily drop privs back to calling + // user (we'll permanently drop after loading seccomp) + if (setegid(real_gid) != 0) + die("setegid failed"); + if (seteuid(real_uid) != 0) + die("seteuid failed"); + + if (real_gid != 0 && geteuid() == 0) + die("dropping privs did not work"); + if (real_uid != 0 && getegid() == 0) + die("dropping privs did not work"); + } + // Ensure that the user data path exists. + setup_user_data(); +#if 0 + setup_user_xdg_runtime_dir(); +#endif + // https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement + sc_maybe_aa_change_onexec(&apparmor, security_tag); +#ifdef HAVE_SECCOMP + sc_apply_seccomp_bpf(security_tag); +#endif // ifdef HAVE_SECCOMP + if (snap_context != NULL) { + setenv("SNAP_COOKIE", snap_context, 1); + // for compatibility, if facing older snapd. + setenv("SNAP_CONTEXT", snap_context, 1); + } + // Permanently drop if not root + if (geteuid() == 0) { + // Note that we do not call setgroups() here because its ok + // that the user keeps the groups he already belongs to + if (setgid(real_gid) != 0) + die("setgid failed"); + if (setuid(real_uid) != 0) + die("setuid failed"); + + if (real_gid != 0 && (getuid() == 0 || geteuid() == 0)) + die("permanently dropping privs did not work"); + if (real_uid != 0 && (getgid() == 0 || getegid() == 0)) + die("permanently dropping privs did not work"); + } + // and exec the new executable + argv[0] = (char *)executable; + debug("execv(%s, %s...)", executable, argv[0]); + for (int i = 1; i < argc; ++i) { + debug(" argv[%i] = %s", i, argv[i]); + } + execv(executable, (char *const *)&argv[0]); + perror("execv failed"); + return 1; +} diff --git a/cmd/snap-confine/snap-confine.rst b/cmd/snap-confine/snap-confine.rst new file mode 100644 index 00000000..678363a2 --- /dev/null +++ b/cmd/snap-confine/snap-confine.rst @@ -0,0 +1,200 @@ +============== + snap-confine +============== + +----------------------------------------------- +internal tool for confining snappy applications +----------------------------------------------- + +:Author: zygmunt.krynicki@canonical.com +:Date: 2017-09-18 +:Copyright: Canonical Ltd. +:Version: 2.28 +:Manual section: 1 +:Manual group: snappy + +SYNOPSIS +======== + + snap-confine [--classic] [--base BASE] SECURITY_TAG COMMAND [...ARGUMENTS] + +DESCRIPTION +=========== + +The `snap-confine` is a program used internally by `snapd` to construct the +execution environment for snap applications. + +OPTIONS +======= + +The `snap-confine` program accepts two options: + + `--classic` requests the so-called _classic_ _confinement_ in which + applications are not confined at all (like in classic systems, hence the + name). This disables the use of a dedicated, per-snap mount namespace. The + `snapd` service generates permissive apparmor and seccomp profiles that + allow everything. + + `--base BASE` directs snap-confine to use the given base snap as the root + filesystem. If omitted it defaults to the `core` snap. This is derived from + snap meta-data by `snapd` when starting the application process. + +FEATURES +======== + +Apparmor profiles +----------------- + +`snap-confine` switches to the apparmor profile `$SECURITY_TAG`. The profile is +**mandatory** and `snap-confine` will refuse to run without it. + +The profile has to be loaded into the kernel prior to using `snap-confine`. +Typically this is arranged for by `snapd`. The profile contains rich +description of what the application process is allowed to do, this includes +system calls, file paths, access patterns, linux capabilities, etc. The +apparmor profile can also do extensive dbus mediation. Refer to apparmor +documentation for more details. + +Seccomp profiles +---------------- + +`snap-confine` looks for the +`/var/lib/snapd/seccomp/bpf/$SECURITY_TAG.bin` file. This file is +**mandatory** and `snap-confine` will refuse to run without it. This +file contains the seccomp bpf binary program that is loaded into the +kernel by snap-confine. + +The file is generated with the `/usr/lib/snapd/snap-seccomp` compiler from the +`$SECURITY_TAG.src` file that uses a custom syntax that describes the set of +allowed system calls and optionally their arguments. The profile is then used +to confine the started application. + +As a security precaution disallowed system calls cause the started application +executable to be killed by the kernel. In the future this restriction may be +lifted to return `EPERM` instead. + +Mount profiles +-------------- + +`snap-confine` uses a helper process, `snap-update-ns`, to apply the mount +namespace profile to freshly constructed mount namespace. That tool looks for +the `/var/lib/snapd/mount/snap.$SNAP_NAME.fstab` file. If present it is read, +parsed and treated like a mostly-typical `fstab(5)` file. The mount directives +listed there are executed in order. All directives must succeed as any failure +will abort execution. + +By default all mount entries start with the following flags: `bind`, `ro`, +`nodev`, `nosuid`. Some of those flags can be reversed by an appropriate +option (e.g. `rw` can cause the mount point to be writable). + +Certain additional features are enabled and conveyed through the use of mount +options prefixed with `x-snapd-`. + +As a security precaution only `bind` mounts are supported at this time. + +Quirks +------ + +`snap-confine` contains a quirk system that emulates some or the behavior of +the older versions of snap-confine that certain snaps (still in devmode but +useful and important) have grown to rely on. This section documents the list of +quirks: + +- The `/var/lib/lxd` directory, if it exists on the host, is made available in + the execution environment. This allows various snaps, while running in + devmode, to access the LXD socket. LP: #1613845 + +Sharing of the mount namespace +------------------------------ + +As of version 1.0.41 all the applications from the same snap will share the +same mount namespace. Applications from different snaps continue to use +separate mount namespaces. + +ENVIRONMENT +=========== + +`snap-confine` responds to the following environment variables + +`SNAP_CONFINE_DEBUG`: + When defined the program will print additional diagnostic information about + the actions being performed. All the output goes to stderr. + +The following variables are only used when `snap-confine` is not setuid root. +This is only applicable when testing the program itself. + +`SNAPPY_LAUNCHER_INSIDE_TESTS`: + Internal variable that should not be relied upon. + +`SNAP_CONFINE_NO_ROOT`: + Internal variable that should not be relied upon. + +`SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR`: + Internal variable that should not be relied upon. + +`SNAP_USER_DATA`: + Full path to the directory like /home/$LOGNAME/snap/$SNAP_NAME/$SNAP_REVISION. + + This directory is created by snap-confine on startup. This is a temporary + feature that will be merged into snapd's snap-run command. The set of directories + that can be created is confined with apparmor. + +FILES +===== + +`snap-confine` and `snap-update-ns` use the following files: + +`/var/lib/snapd/mount/snap.*.fstab`: + + Description of the mount profile. + +`/var/lib/snapd/seccomp/bpf/*.src`: + + Input for the /usr/lib/snapd/snap-seccomp profile compiler. + +`/var/lib/snapd/seccomp/bpf/*.bin`: + + Compiled seccomp bpf profile programs. + +`/run/snapd/ns/`: + + Directory used to keep shared mount namespaces. + + `snap-confine` internally converts this directory to a private bind mount. + Semantically the behavior is identical to the following mount commands: + + mount --bind /run/snapd/ns /run/snapd/ns + mount --make-private /run/snapd/ns + +`/run/snapd/ns/.lock`: + + A `flock(2)`-based lock file acquired to create and convert + `/run/snapd/ns/` to a private bind mount. + +`/run/snapd/ns/$SNAP_NAME.lock`: + + A `flock(2)`-based lock file acquired to create or join the mount namespace + represented as `/run/snaps/ns/$SNAP_NAME.mnt`. + +`/run/snapd/ns/$SNAP_NAME.mnt`: + + This file can be either: + + - An empty file that may be seen before the mount namespace is preserved or + when the mount namespace is unmounted. + - A file belonging to the `nsfs` file system, representing a fully + populated mount namespace of a given snap. The file is bind mounted from + `/proc/self/ns/mnt` from the first process in any snap. + +`/proc/self/mountinfo`: + + This file is read to decide if `/run/snapd/ns/` needs to be created and + converted to a private bind mount, as described above. + +Note that the apparmor profile is external to `snap-confine` and is loaded +directly into the kernel. The actual apparmor profile is managed by `snapd`. + +BUGS +==== + +Please report all bugs with https://bugs.launchpad.net/snap-confine/+filebug diff --git a/cmd/snap-confine/snappy-app-dev b/cmd/snap-confine/snappy-app-dev new file mode 100755 index 00000000..6d75549f --- /dev/null +++ b/cmd/snap-confine/snappy-app-dev @@ -0,0 +1,39 @@ +#!/bin/sh +# udev callout (should live in /lib/udev) to allow a snap to access a device node +set -e +# debugging +#exec >>/tmp/snappy-app-dev.log +#exec 2>&1 +#set -x +# end debugging + +ACTION="$1" +APPNAME="$2" +DEVPATH="$3" +MAJMIN="$4" +[ -n "$APPNAME" ] || { echo "no app name given" >&2; exit 1; } +[ -n "$DEVPATH" ] || { echo "no devpath given" >&2; exit 1; } +[ -n "$MAJMIN" ] || { echo "no major/minor given" >&2; exit 0; } + +APPNAME="$( echo "$APPNAME" | tr '_' '.' )" +app_dev_cgroup="/sys/fs/cgroup/devices/$APPNAME" + +# check if it's a block or char dev +if [ "${DEVPATH#*/block/}" != "$DEVPATH" ]; then + type="b" +else + type="c" +fi + +acl="$type $MAJMIN rwm" +case "$ACTION" in + add|change) + echo "$acl" > "$app_dev_cgroup/devices.allow" + ;; + remove) + echo "$acl" > "$app_dev_cgroup/devices.deny" + ;; + *) + echo "ERROR: unknown action $ACTION" >&2 + exit 1 ;; +esac diff --git a/cmd/snap-confine/spread-tests/data/apt-keys/README.md b/cmd/snap-confine/spread-tests/data/apt-keys/README.md new file mode 100644 index 00000000..e496ac7c --- /dev/null +++ b/cmd/snap-confine/spread-tests/data/apt-keys/README.md @@ -0,0 +1,4 @@ +This directory contains keys used by the sbuild program to sign the temporary +archive. Those keys are kept in the tree as ephemeral test virtual machines do +not have sufficient entropy to generate keys by themselves in reasonable amount +of time. diff --git a/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.pub b/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.pub new file mode 100644 index 0000000000000000000000000000000000000000..34d8b57690b7e8133b73a799e4bd470e7c47d8f6 GIT binary patch literal 427 zcmV;c0aX5&jRaN%NFV_O0JxNLP0VZb%PQy4fPWY1qHz=2-OFRY>?qI^dQplJc^zEW zlyRWPtL6IdhsvnpS3$?APa};lq4O_Ap2#WX{ICdgZ<>h{OB+QR{8T$<)it#RQ;UDv zo1&Xvq_!6MnVPkhDzPZp-u_v+iG@fsOst&m@-QZ{HYLBpm|BMB0RRyJ00FdMQ(|># zY-Au)X=iR_av&&EVs&Y3WFSIyX>4R5L}hSgZe(R{V|gG!a${&|c4Z(-WqBzeJYsce zY-D6DbZ>8Lb1h_Lc4cfpY-w|Jb1q?QX>W9BE@Wk5X<=?IZ*pfoh`0n30RjLb1p-zC zNFV|mF9r(<2nPcK1{DYb2?`4Y76JnS0v-VZ7k~f?2@nT;U_;3Udp>|o1OSx3R@!!G zeO2yv*U_X3=WZ+PDC*k<$x^81WV$O(@N(X!KIC@!eb=xSnKhFGzZ}c=yL0 zrd@Jf1&OKj`DLF)gj<}2xF~|^ufn$emLArzJ30U+BH$|lghxX(opymUmyCa05JsK* VihyKrl%!aG4JdUEL$`+VD?>j~u}}a2 literal 0 HcmV?d00001 diff --git a/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.sec b/cmd/snap-confine/spread-tests/data/apt-keys/sbuild-key.sec new file mode 100644 index 0000000000000000000000000000000000000000..cdadd277936c5971b14b3e4006a9e202c0408c60 GIT binary patch literal 759 zcmV7sEH+TF`zzw9W`6M9jK5_uh5 z*OYOf$E)S~?}y5$;#Wb(sZS$~FQM}e1yhTE z+MA-AU!=Ac`I(xvmMXC*+1~zHxrv2HG)%0V@A5DvvNk2Z!kAiy<^cc^0RRC21N;o< z(laL6Ac08}a>QLeh=(IpVovfd+y^k$RhmwdvlVqrUBzPweH4R?zfCm?2xC?C)*N!J zr#LZI>x}))QYUeTN7*H>_wA$Z!mrR5Ym!s!Pz__GhS~vAuldxk0qCXNQ`7|L(}~1% z?$J&%)ffS5L6|LIX*f>Q&JG+$Cu4q^ly+uF#lxhVnv zU2hs`Jn!Ui>z0s`V=50{4nki!=B-RCbJFh#OZz5>PKLlT!5vD$`J{|^)OJMoliKLL zmu#8!wAyKlBNJN*o=vo1Q(|>#Y-Au)X=iR_av&&EVs&Y3WFSIyX>4R5L}hSgZe(R{ zV|gG!a${&|c4Z(-WqBzeJYsceY-D6DbZ>8Lb1h_Lc4cfpY-w|Jb1q?QX>W9BE@Wk5 zX<=?IZ*pfoh`0n30RjLb1p-zCNFV|mF9r(<2nPcK1{DYb2?`4Y76JnS0v-VZ7k~f? z2@nT;U_;3Udp>|o1OSx3R@!!GeO2yv*U_X3=WZ+PDC*k<$x^81WV$O(@N(X!KIC@! zeb=xSnKhFGzZ}c=yL0rd@Jf1&OKj`DLF)gj<}2xF~|^ufn$emLArzJ30U+ pBH$|lghxX(opymUmyCa05JsK*ihyKrl%!aG4JdUEL$`+VD?^GfP~ZRn literal 0 HcmV?d00001 diff --git a/cmd/snap-confine/spread-tests/distros/debian. b/cmd/snap-confine/spread-tests/distros/debian. new file mode 100644 index 00000000..4a5a9eb5 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/debian. @@ -0,0 +1,2 @@ +distro_codename=sid +distro_packaging_git_branch=debian diff --git a/cmd/snap-confine/spread-tests/distros/debian.common b/cmd/snap-confine/spread-tests/distros/debian.common new file mode 100644 index 00000000..b6084467 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/debian.common @@ -0,0 +1,12 @@ +if [ -n "${APT_PROXY:-}" ]; then + distro_archive=${APT_PROXY}/ftp.debian.org/debian +else + distro_archive=http://ftp.debian.org/debian +fi +# NOTE: Debian packaging needs to be updated. I sent a mail to the +# debian maintainer with instructions on what needs to happen and +# how it fits into the CI system. +# +# For now all builds on debian will fail as they still contains +# debian/patches that are now applied upstream. +distro_packaging_git=git://anonscm.debian.org/collab-maint/snap-confine.git diff --git a/cmd/snap-confine/spread-tests/distros/ubuntu.14.04 b/cmd/snap-confine/spread-tests/distros/ubuntu.14.04 new file mode 100644 index 00000000..8471d577 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/ubuntu.14.04 @@ -0,0 +1,2 @@ +distro_codename=trusty +distro_packaging_git_branch=14.04 diff --git a/cmd/snap-confine/spread-tests/distros/ubuntu.16.04 b/cmd/snap-confine/spread-tests/distros/ubuntu.16.04 new file mode 100644 index 00000000..4e89a355 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/ubuntu.16.04 @@ -0,0 +1,2 @@ +distro_codename=xenial +distro_packaging_git_branch=16.04 diff --git a/cmd/snap-confine/spread-tests/distros/ubuntu.16.10 b/cmd/snap-confine/spread-tests/distros/ubuntu.16.10 new file mode 100644 index 00000000..374ea2e6 --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/ubuntu.16.10 @@ -0,0 +1,2 @@ +distro_codename=yakkety +distro_packaging_git_branch=16.10 diff --git a/cmd/snap-confine/spread-tests/distros/ubuntu.common b/cmd/snap-confine/spread-tests/distros/ubuntu.common new file mode 100644 index 00000000..4cb0db9d --- /dev/null +++ b/cmd/snap-confine/spread-tests/distros/ubuntu.common @@ -0,0 +1,7 @@ +if [ -n "${APT_PROXY:-}" ]; then + distro_archive=${APT_PROXY}/archive.ubuntu.com/ubuntu +else + distro_archive=http://archive.ubuntu.com/ubuntu +fi +distro_packaging_git=https://git.launchpad.net/snap-confine +sbuild_createchroot_extra="--components=main,universe" diff --git a/cmd/snap-confine/spread-tests/main/cgroup-used/task.yaml b/cmd/snap-confine/spread-tests/main/cgroup-used/task.yaml new file mode 100644 index 00000000..4c0c9272 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/cgroup-used/task.yaml @@ -0,0 +1,37 @@ +summary: Check that launcher cgroup functionality works +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Install snapd-hacker-toolbelt" + snap install snapd-hacker-toolbelt +execute: | + cd / + echo "Clear udev tags and cgroups with non-test device and running snapd-hacker-toolbelt.busybox" + echo 'KERNEL=="uinput", TAG+="snap_snapd-hacker-toolbelt_busybox"' > /etc/udev/rules.d/70-spread-test.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello + echo "Verify no tags for snapd-hacker-toolbelt.busybox for kmsg" + if udevadm info /sys/devices/virtual/mem/kmsg | grep snap_snapd-hacker-toolbelt_busybox ; then exit 1; fi + echo "Manually add udev tags for snapd-hacker-toolbelt.busybox for kmsg" + echo 'KERNEL=="kmsg", TAG+="snap_snapd-hacker-toolbelt_busybox"' > /etc/udev/rules.d/70-spread-test.rules + echo "Simulate snapd udev triggers" + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + echo "Verify udev has tag for kmsg" + if ! udevadm info /sys/devices/virtual/mem/kmsg | grep snap_snapd-hacker-toolbelt_busybox; then exit 1; fi + echo "Run snapd-hacker-toolbelt.busybox echo and see if kmsg added to cgroup" + snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello + if ! grep 'c 1:11 rwm' /sys/fs/cgroup/devices/snap.snapd-hacker-toolbelt.busybox/devices.list ; then exit 1; fi +restore: | + snap remove snapd-hacker-toolbelt + rm -f /etc/udev/rules.d/70-spread-test.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + # no way to clear cgroup for snapd-hacker-toolbelt atm diff --git a/cmd/snap-confine/spread-tests/main/core-is-preferred/task.yaml b/cmd/snap-confine/spread-tests/main/core-is-preferred/task.yaml new file mode 100644 index 00000000..c9448ba1 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/core-is-preferred/task.yaml @@ -0,0 +1,10 @@ +summary: The snap named 'core' is preferred to the snap 'ubuntu-core' +prepare: | + snap install --devmode snapd-hacker-toolbelt + snap install core +execute: | + snapd-hacker-toolbelt.busybox cat /meta/snap.yaml | grep -q -F 'name: core' +restore: | + snap remove snapd-hacker-toolbelt + # XXX: the core snap cannot be removed, we should use a trick to remove it + # in some other way but this can wait. diff --git a/cmd/snap-confine/spread-tests/main/debug-flags/task.yaml b/cmd/snap-confine/spread-tests/main/debug-flags/task.yaml new file mode 100644 index 00000000..a95b86be --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/debug-flags/task.yaml @@ -0,0 +1,12 @@ +summary: snap-confine honors SNAP_CONFINE_DEBUG environment variable +execute: | + for value in yes no 0 1 unicorn; do + SNAP_CONFINE_DEBUG=$value ubuntu-core-launcher blah 2>debug.$value || : + done + grep -F -q 'DEBUG: shifting arguments by one' debug.yes + grep -F -q 'DEBUG: shifting arguments by one' debug.1 + grep -F -v -q 'DEBUG: shifting arguments by one' debug.no + grep -F -v -q 'DEBUG: shifting arguments by one' debug.0 + grep -F -q 'WARNING: unrecognized value of environment variable SNAP_CONFINE_DEBUG (expected yes/no or 1/0)' debug.unicorn +restore: | + rm -f debug.* diff --git a/cmd/snap-confine/spread-tests/main/hostfs-created-on-demand/task.yaml b/cmd/snap-confine/spread-tests/main/hostfs-created-on-demand/task.yaml new file mode 100644 index 00000000..8049b5af --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/hostfs-created-on-demand/task.yaml @@ -0,0 +1,24 @@ +summary: Check that /var/lib/snapd/hostfs is created on demand +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + The /var/lib/snapd/hostfs directory is created by snap-confine + if the host packaging of snapd doesn't already provide it. +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can move the packaged hostfs directory aside" + if [ -d /var/lib/snapd/hostfs ]; then + mv /var/lib/snapd/hostfs /var/lib/snapd/hostfs.orig + fi +execute: | + cd / + echo "We can now run a busybox true just to ensure it started correctly" + /snap/bin/snapd-hacker-toolbelt.busybox true + echo "We can now check that the directory was created on the system" + test -d /var/lib/snapd/hostfs +restore: | + snap remove snapd-hacker-toolbelt + if [ -d /var/lib/snapd/hostfs.orig ]; then + mv /var/lib/snapd/hostfs.orig /var/lib/snapd/hostfs + fi diff --git a/cmd/snap-confine/spread-tests/main/media-visible-in-devmode/task.yaml b/cmd/snap-confine/spread-tests/main/media-visible-in-devmode/task.yaml new file mode 100644 index 00000000..6f4a0868 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/media-visible-in-devmode/task.yaml @@ -0,0 +1,15 @@ +summary: Check that /media is available to snaps installed in --devmode +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap in devmode" + snap install --devmode snapd-hacker-toolbelt + echo "Having added a canary file in /media" + echo "test" > /media/canary +execute: | + cd / + echo "We can see the canary file in /media" + [ "$(snapd-hacker-toolbelt.busybox cat /media/canary)" = "test" ] +restore: | + snap remove snapd-hacker-toolbelt + rm -f /media/canary diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.json new file mode 100644 index 00000000..1ac5d316 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.json @@ -0,0 +1,1028 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/var/lib/snapd/hostfs/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.amd64.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.amd64.json new file mode 100644 index 00000000..9d66da7f --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.amd64.json @@ -0,0 +1,798 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.i386.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.i386.json new file mode 100644 index 00000000..f9d417b3 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.linode.i386.json @@ -0,0 +1,808 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "fuse.lxcfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/lxcfs", + "mount_src": "lxcfs", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.amd64.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.amd64.json new file mode 100644 index 00000000..67a7c2ba --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.amd64.json @@ -0,0 +1,808 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/lxd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/lxd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.i386.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.i386.json new file mode 100644 index 00000000..67a7c2ba --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.core.qemu.i386.json @@ -0,0 +1,808 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/lxd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/lxd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.json new file mode 100644 index 00000000..209433e0 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.json @@ -0,0 +1,1028 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/var/lib/snapd/hostfs/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.amd64.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.amd64.json new file mode 100644 index 00000000..aa6fb331 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.amd64.json @@ -0,0 +1,788 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.i386.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.i386.json new file mode 100644 index 00000000..a7ebcdf5 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.linode.i386.json @@ -0,0 +1,798 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "fuse.lxcfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/lxcfs", + "mount_src": "lxcfs", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,noatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.amd64.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.amd64.json new file mode 100644 index 00000000..0eb9e335 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.amd64.json @@ -0,0 +1,798 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/lxd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/lxd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.i386.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.i386.json new file mode 100644 index 00000000..0eb9e335 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.classic.ubuntu-core.qemu.i386.json @@ -0,0 +1,798 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/etc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/home" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/lib/modules" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/media", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "shared:renumbered/7" + ], + "root_dir": "/media" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "ro,nosuid,nodev,noexec", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/tmp/snap.0_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/src", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/usr/src" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib", + "mount_src": "none", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/apparmor" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/classic", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/classic" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/cloud" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/console-conf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dbus" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/dhcp" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/extrausers" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initramfs-tools" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initscripts", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/initscripts" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/insserv", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/insserv" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/lxd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/lxd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/machines", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/machines" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/misc" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/pam", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/pam" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/python", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/python" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/resolvconf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/resolvconf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK1", + "opt_fields": [], + "root_dir": "/var/lib/snapd/hostfs" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/NUMBER", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/ubuntu-core/NUMBER", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/sudo" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/systemd" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ubuntu-fan", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ubuntu-fan" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/ucf", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/ucf" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/update-rc.d", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/update-rc.d" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/urandom", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/urandom" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/vim", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/vim" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK1", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/var/tmp" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.json new file mode 100644 index 00000000..2f7fdc1f --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.json @@ -0,0 +1,2050 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/boot/efi", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/boot/grub", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/EFI/ubuntu" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/apparmor.d/cache", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/7" + ], + "root_dir": "/system-data/etc/apparmor.d/cache" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/system-data/etc/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/dbus-1/system.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/system-data/etc/dbus-1/system.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/default/keyboard", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/system-data/etc/default/keyboard" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/etc/fstab", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/image.fstab" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/hosts", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/system-data/etc/hosts" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/machine-id", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/system-data/etc/machine-id" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/modprobe.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/system-data/etc/modprobe.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/modules-load.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/system-data/etc/modules-load.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/netplan", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/system-data/etc/netplan" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/network/if-up.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/system-data/etc/network/if-up.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/network/interfaces.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/system-data/etc/network/interfaces.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ppp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/system-data/etc/ppp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ssh", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/system-data/etc/ssh" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/sudoers.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/system-data/etc/sudoers.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/sysctl.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/system-data/etc/sysctl.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/network", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/system-data/etc/systemd/network" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/system-data/etc/systemd/system.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/user.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/system-data/etc/systemd/user.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/udev/rules.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/system-data/etc/udev/rules.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ufw", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/system-data/etc/ufw" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/system-data/etc/writable" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/33" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/34" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/35" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/media", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "shared:renumbered/36" + ], + "root_dir": "/media" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/mnt", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/37" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/38" + ], + "root_dir": "/" + }, + { + "fs_type": "binfmt_misc", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "binfmt_misc", + "opt_fields": [ + "master:renumbered/40" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/39" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/41" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/42" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/run/cgmanager/fs", + "mount_src": "cgmfs", + "opt_fields": [ + "master:renumbered/43" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/44" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/45" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/46" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/50" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/51" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/52" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/53" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/54" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/55" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/56" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/57" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/58" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/59" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/60" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/61" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/62" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/63" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/64" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/65" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/66" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snap.1001_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp/snap.rootfs_XXXXXX", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/snap.rootfs_XXXXXX" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/lib/snapd/snap-confine", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data/zyga/snap-confine" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/cache/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/cache/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/console-conf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dbus" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dhcp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/extrausers" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/initramfs-tools" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/misc" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK3", + "opt_fields": [], + "root_dir": "/system-data/var/lib/snapd/hostfs" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/boot/efi", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/boot/grub", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/EFI/ubuntu" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/var/lib/snapd/hostfs/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/apparmor.d/cache", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/7" + ], + "root_dir": "/system-data/etc/apparmor.d/cache" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/system-data/etc/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/dbus-1/system.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/system-data/etc/dbus-1/system.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/default/keyboard", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/system-data/etc/default/keyboard" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/fstab", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/image.fstab" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/hosts", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/system-data/etc/hosts" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/machine-id", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/system-data/etc/machine-id" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/modprobe.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/system-data/etc/modprobe.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/modules-load.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/system-data/etc/modules-load.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/netplan", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/system-data/etc/netplan" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/network/if-up.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/system-data/etc/network/if-up.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/network/interfaces.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/system-data/etc/network/interfaces.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ppp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/system-data/etc/ppp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ssh", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/system-data/etc/ssh" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/sudoers.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/system-data/etc/sudoers.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/sysctl.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/system-data/etc/sysctl.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/network", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/system-data/etc/systemd/network" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/system-data/etc/systemd/system.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/user.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/system-data/etc/systemd/user.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/udev/rules.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/system-data/etc/udev/rules.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ufw", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/system-data/etc/ufw" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/system-data/etc/writable" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/home", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/33" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/34" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/35" + ], + "root_dir": "/modules" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/mnt", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/37" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/38" + ], + "root_dir": "/" + }, + { + "fs_type": "binfmt_misc", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc", + "mount_src": "binfmt_misc", + "opt_fields": [ + "master:renumbered/40" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/39" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/root", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/41" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/42" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/cgmanager/fs", + "mount_src": "cgmfs", + "opt_fields": [ + "master:renumbered/43" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/44" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/45" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/46" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/50" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/51" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/52" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/53" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/54" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/55" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/56" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/57" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/58" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/59" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/60" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/61" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/62" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/63" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/64" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/65" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/66" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/tmp", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/tmp/snap.rootfs_XXXXXX", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/snap.rootfs_XXXXXX" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/usr/lib/snapd/snap-confine", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data/zyga/snap-confine" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/cache/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/cache/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/console-conf", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/console-conf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/dbus", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dbus" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/dhcp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dhcp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/extrausers", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/extrausers" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/initramfs-tools", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/initramfs-tools" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/logrotate", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/misc", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/misc" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/snapd", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/snapd" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/sudo", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/70" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/random-seed", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/random-seed" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/rfkill", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/rfkill" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/waagent", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/log", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/tmp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/70" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd/random-seed", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/random-seed" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd/rfkill", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/rfkill" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.linode.json b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.linode.json new file mode 100644 index 00000000..93d8d808 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/expected.core.linode.json @@ -0,0 +1,1800 @@ +[ + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/boot/efi", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/boot/grub", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/EFI/ubuntu" + }, + { + "fs_type": "devtmpfs", + "mount_opts": "rw,nosuid,relatime", + "mount_point": "/dev", + "mount_src": "udev", + "opt_fields": [ + "master:renumbered/2" + ], + "root_dir": "/" + }, + { + "fs_type": "hugetlbfs", + "mount_opts": "rw,relatime", + "mount_point": "/dev/hugepages", + "mount_src": "hugetlbfs", + "opt_fields": [ + "master:renumbered/3" + ], + "root_dir": "/" + }, + { + "fs_type": "mqueue", + "mount_opts": "rw,relatime", + "mount_point": "/dev/mqueue", + "mount_src": "mqueue", + "opt_fields": [ + "master:renumbered/4" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/ptmx", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/ptmx" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [ + "master:renumbered/5" + ], + "root_dir": "/" + }, + { + "fs_type": "devpts", + "mount_opts": "rw,relatime", + "mount_point": "/dev/pts", + "mount_src": "devpts", + "opt_fields": [], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev", + "mount_point": "/dev/shm", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/6" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/etc/alternatives", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/etc/alternatives" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/apparmor.d/cache", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/7" + ], + "root_dir": "/system-data/etc/apparmor.d/cache" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/system-data/etc/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/dbus-1/system.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/system-data/etc/dbus-1/system.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/default/keyboard", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/system-data/etc/default/keyboard" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/etc/fstab", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/image.fstab" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/hosts", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/system-data/etc/hosts" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/machine-id", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/system-data/etc/machine-id" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/modprobe.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/system-data/etc/modprobe.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/modules-load.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/system-data/etc/modules-load.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/netplan", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/system-data/etc/netplan" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/network/if-up.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/system-data/etc/network/if-up.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/network/interfaces.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/system-data/etc/network/interfaces.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ppp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/system-data/etc/ppp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ssh", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/system-data/etc/ssh" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/sudoers.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/system-data/etc/sudoers.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/sysctl.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/system-data/etc/sysctl.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/network", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/system-data/etc/systemd/network" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/system.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/system-data/etc/systemd/system.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/systemd/user.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/system-data/etc/systemd/user.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/udev/rules.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/system-data/etc/udev/rules.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/ufw", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/system-data/etc/ufw" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/etc/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/system-data/etc/writable" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/home", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/33" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/34" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/35" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/media", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "shared:renumbered/36" + ], + "root_dir": "/media" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/mnt", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/37" + ], + "root_dir": "/" + }, + { + "fs_type": "proc", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/proc", + "mount_src": "proc", + "opt_fields": [ + "master:renumbered/38" + ], + "root_dir": "/" + }, + { + "fs_type": "binfmt_misc", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "binfmt_misc", + "opt_fields": [ + "master:renumbered/40" + ], + "root_dir": "/" + }, + { + "fs_type": "autofs", + "mount_opts": "rw,relatime", + "mount_point": "/proc/sys/fs/binfmt_misc", + "mount_src": "systemd-1", + "opt_fields": [ + "master:renumbered/39" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/root", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/41" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/42" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/run/cgmanager/fs", + "mount_src": "cgmfs", + "opt_fields": [ + "master:renumbered/43" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/44" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/45" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/46" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "sysfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys", + "mount_src": "sysfs", + "opt_fields": [ + "master:renumbered/50" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw", + "mount_point": "/sys/fs/cgroup", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/51" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/blkio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/52" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpu,cpuacct", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/53" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/cpuset", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/54" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/devices", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/55" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/freezer", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/56" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/hugetlb", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/57" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/memory", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/58" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/net_cls,net_prio", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/59" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/perf_event", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/60" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/pids", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/61" + ], + "root_dir": "/" + }, + { + "fs_type": "cgroup", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/cgroup/systemd", + "mount_src": "cgroup", + "opt_fields": [ + "master:renumbered/62" + ], + "root_dir": "/" + }, + { + "fs_type": "fusectl", + "mount_opts": "rw,relatime", + "mount_point": "/sys/fs/fuse/connections", + "mount_src": "fusectl", + "opt_fields": [ + "master:renumbered/63" + ], + "root_dir": "/" + }, + { + "fs_type": "pstore", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/fs/pstore", + "mount_src": "pstore", + "opt_fields": [ + "master:renumbered/64" + ], + "root_dir": "/" + }, + { + "fs_type": "debugfs", + "mount_opts": "rw,relatime", + "mount_point": "/sys/kernel/debug", + "mount_src": "debugfs", + "opt_fields": [ + "master:renumbered/65" + ], + "root_dir": "/" + }, + { + "fs_type": "securityfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/sys/kernel/security", + "mount_src": "securityfs", + "opt_fields": [ + "master:renumbered/66" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snap.1001_snap.snapd-hacker-toolbelt.busybox_XXXXXX/tmp" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/tmp/snap.rootfs_XXXXXX", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/snap.rootfs_XXXXXX" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/usr/lib/snapd/snap-confine", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data/zyga/snap-confine" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/cache/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/cache/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/console-conf", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/console-conf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dbus", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dbus" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/dhcp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dhcp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/extrausers", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/extrausers" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/initramfs-tools", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/initramfs-tools" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/logrotate", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/misc", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/misc" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/snapd" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/BLOCK3", + "opt_fields": [], + "root_dir": "/system-data/var/lib/snapd/hostfs" + }, + { + "fs_type": "squashfs", + "mount_opts": "ro,relatime", + "mount_point": "/var/lib/snapd/hostfs", + "mount_src": "/dev/remapped-loop0", + "opt_fields": [ + "master:renumbered/0" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/boot/efi", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/" + }, + { + "fs_type": "vfat", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/boot/grub", + "mount_src": "/dev/BLOCK2", + "opt_fields": [ + "master:renumbered/1" + ], + "root_dir": "/EFI/ubuntu" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/apparmor.d/cache", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/7" + ], + "root_dir": "/system-data/etc/apparmor.d/cache" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/8" + ], + "root_dir": "/system-data/etc/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/dbus-1/system.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/9" + ], + "root_dir": "/system-data/etc/dbus-1/system.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/default/keyboard", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/10" + ], + "root_dir": "/system-data/etc/default/keyboard" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/fstab", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/11" + ], + "root_dir": "/image.fstab" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/hosts", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/12" + ], + "root_dir": "/system-data/etc/hosts" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/machine-id", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/13" + ], + "root_dir": "/system-data/etc/machine-id" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/modprobe.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/14" + ], + "root_dir": "/system-data/etc/modprobe.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/modules-load.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/15" + ], + "root_dir": "/system-data/etc/modules-load.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/netplan", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/16" + ], + "root_dir": "/system-data/etc/netplan" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/network/if-up.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/17" + ], + "root_dir": "/system-data/etc/network/if-up.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/network/interfaces.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/18" + ], + "root_dir": "/system-data/etc/network/interfaces.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ppp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/19" + ], + "root_dir": "/system-data/etc/ppp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ssh", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/20" + ], + "root_dir": "/system-data/etc/ssh" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/sudoers.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/21" + ], + "root_dir": "/system-data/etc/sudoers.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/sysctl.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/22" + ], + "root_dir": "/system-data/etc/sysctl.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/network", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/23" + ], + "root_dir": "/system-data/etc/systemd/network" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/24" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/25" + ], + "root_dir": "/system-data/etc/systemd/system" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/system.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/26" + ], + "root_dir": "/system-data/etc/systemd/system.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/systemd/user.conf.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/27" + ], + "root_dir": "/system-data/etc/systemd/user.conf.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/udev/rules.d", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/28" + ], + "root_dir": "/system-data/etc/udev/rules.d" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/ufw", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/29" + ], + "root_dir": "/system-data/etc/ufw" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/etc/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/30" + ], + "root_dir": "/system-data/etc/writable" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/home", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/32" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/firmware", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/33" + ], + "root_dir": "/firmware" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/34" + ], + "root_dir": "/modules" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/lib/modules", + "mount_src": "/dev/remapped-loop1", + "opt_fields": [ + "master:renumbered/35" + ], + "root_dir": "/modules" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/mnt", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/37" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/root", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/root" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/41" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/42" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/cgmanager/fs", + "mount_src": "cgmfs", + "opt_fields": [ + "master:renumbered/43" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/lock", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/44" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,noexec,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/snapd/ns", + "mount_src": "tmpfs", + "opt_fields": [], + "root_dir": "/snapd/ns" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/45" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,nosuid,nodev,relatime", + "mount_point": "/var/lib/snapd/hostfs/run/user/NUMBER", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/46" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/snap" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/tmp", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/tmp/snap.rootfs_XXXXXX", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/67" + ], + "root_dir": "/snap.rootfs_XXXXXX" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/usr/lib/snapd/snap-confine", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/user-data/zyga/snap-confine" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/cache/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/cache/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/apparmor", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/apparmor" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/cloud", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/cloud" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/console-conf", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/console-conf" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/dbus", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dbus" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/dhcp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/dhcp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/extrausers", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/extrausers" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/initramfs-tools", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/initramfs-tools" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/logrotate", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/logrotate" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/misc", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/misc" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/snapd", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/snapd" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/sudo", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/70" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/random-seed", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/random-seed" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/systemd/rfkill", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/rfkill" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/lib/waagent", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/log", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/var/tmp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/snapd/hostfs/writable/system-data/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + }, + { + "fs_type": "tmpfs", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/sudo", + "mount_src": "tmpfs", + "opt_fields": [ + "master:renumbered/70" + ], + "root_dir": "/" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd/random-seed", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/random-seed" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/systemd/rfkill", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/systemd/rfkill" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/lib/waagent", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/lib/waagent" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/log", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/log" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/snap", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/snap" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/var/tmp", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/system-data/var/tmp" + }, + { + "fs_type": "ext4", + "mount_opts": "rw,relatime", + "mount_point": "/writable", + "mount_src": "/dev/BLOCK3", + "opt_fields": [ + "master:renumbered/31" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop5", + "opt_fields": [ + "master:renumbered/68" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/core/NUMBER", + "mount_src": "/dev/remapped-loop6", + "opt_fields": [ + "master:renumbered/69" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/pc-kernel/NUMBER", + "mount_src": "/dev/remapped-loop2", + "opt_fields": [ + "master:renumbered/47" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/pc/NUMBER", + "mount_src": "/dev/remapped-loop3", + "opt_fields": [ + "master:renumbered/48" + ], + "root_dir": "/" + }, + { + "fs_type": "squashfs", + "mount_opts": "rw,relatime", + "mount_point": "/writable/system-data/snap/snapd-hacker-toolbelt/x1", + "mount_src": "/dev/remapped-loop4", + "opt_fields": [ + "master:renumbered/49" + ], + "root_dir": "/" + } +] diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/process.py b/cmd/snap-confine/spread-tests/main/mount-ns-layout/process.py new file mode 100755 index 00000000..1e77afc9 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/process.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +import sys +import json +import re + +class mountinfo_entry: + + def __init__(self, fs_type, mount_id, mount_opts, mount_point, mount_src, opt_fields, root_dir): + self.fs_type = fs_type + self.mount_id = mount_id + self.mount_opts = mount_opts + self.mount_point = mount_point + self.mount_src = mount_src + self.opt_fields = opt_fields + self.root_dir = root_dir + + @classmethod + def parse(cls, line): + parts = line.split() + fs_type = parts[-3] + mount_id = parts[0] + mount_opts = parts[5] + mount_point = parts[4] + mount_src = parts[-2] + root_dir = parts[3] + opt_fields = [] + i = 6 + while parts[i] != '-': + opt = parts[i] + opt_fields.append(opt) + i += 1 + opt_fields.sort() + return cls(fs_type, mount_id, mount_opts, mount_point, mount_src, + opt_fields, root_dir) + + def _fix_nondeterministic_mount_point(self): + self.mount_point = re.sub('_\w{6}', '_XXXXXX', self.mount_point) + self.mount_point = re.sub('/\d+$', '/NUMBER', self.mount_point) + + def _fix_nondeterministic_root_dir(self): + self.root_dir = re.sub('_\w{6}', '_XXXXXX', self.root_dir) + + def _fix_nondeterministic_mount_src(self): + self.mount_src = re.sub('/dev/[sv]da', '/dev/BLOCK', self.mount_src) + + def _fix_nondeterministic_opt_fields(self, seen): + fixed = [] + for opt in self.opt_fields: + if opt not in seen: + opt_id = len(seen) + seen[opt] = opt_id + else: + opt_id = seen[opt] + remapped_opt = re.sub(':\d+$', lambda m: ':renumbered/{}'.format(opt_id), opt) + fixed.append(remapped_opt) + self.opt_fields = fixed + + def _fix_nondeterministic_loop(self, seen): + if not self.mount_src.startswith("/dev/loop"): + return + if self.mount_src not in seen: + loop_id = len(seen) + seen[self.mount_src] = loop_id + else: + loop_id = seen[self.mount_src] + self.mount_src = re.sub('loop\d+$', lambda m: 'remapped-loop{}'.format(loop_id), self.mount_src) + + def as_json(self): + return { + "fs_type": self.fs_type, + "mount_opts": self.mount_opts, + "mount_point": self.mount_point, + "mount_src": self.mount_src, + "opt_fields": self.opt_fields, + "root_dir": self.root_dir, + } + + +def parse_mountinfo(lines): + return [mountinfo_entry.parse(line) for line in lines] + + +def fix_initial_nondeterminism(entries): + for entry in entries: + entry._fix_nondeterministic_mount_point() + + +def fix_remaining_nondeterminism(entries): + seen_opt_fields = {} + seen_loops = {} + for entry in entries: + entry._fix_nondeterministic_root_dir() + entry._fix_nondeterministic_mount_src() + entry._fix_nondeterministic_opt_fields(seen_opt_fields) + entry._fix_nondeterministic_loop(seen_loops) + + +def main(): + entries = parse_mountinfo(sys.stdin) + # Get rid of the core snap as it is not certain that we'll see one and we want determinism + entries = [entry for entry in entries if not re.match("/snap/core/\d+", entry.mount_point)] + # Fix random directories and non-deterministic revisions + fix_initial_nondeterminism(entries) + # Sort by just the mount point, + entries.sort(key=lambda entry: (entry.mount_point)) + # Fix remainder of the non-determinism + fix_remaining_nondeterminism(entries) + # Make entries nicely deterministic, by sorting them by mount location + entries.sort(key=lambda entry: (entry.mount_point, entry.mount_src, entry.root_dir)) + # Export everything + json.dump([entry.as_json() for entry in entries], + sys.stdout, sort_keys=True, indent=2, separators=(',', ': ')) + sys.stdout.write('\n') + + +if __name__ == '__main__': + main() diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/snap-arch.py b/cmd/snap-confine/spread-tests/main/mount-ns-layout/snap-arch.py new file mode 100755 index 00000000..0e0d8ad3 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/snap-arch.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import os +import sys + +def main(): + kernel_arch = os.uname().machine + # Because off by one bugs and naming ... + snap_arch_map = { + 'aarch64': 'arm64', + 'armv7l': 'armhf', + 'x86_64': 'amd64', + 'i686': 'i386', + } + try: + print(snap_arch_map[kernel_arch]) + except KeyError: + print("unsupported kernel architecture: {!a}".format(kernel_arch), file=sys.stderr) + return 1 + + +if __name__ == '__main__': + main() diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-layout/task.yaml b/cmd/snap-confine/spread-tests/main/mount-ns-layout/task.yaml new file mode 100644 index 00000000..859fe7c1 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-layout/task.yaml @@ -0,0 +1,46 @@ +summary: Ensure that the mount namespace a given layout +details: | + This test analyzes /proc/self/mountinfo which contains a representation of + the mount table of the current process. The mount table is a very sensitive + part of the confinement design. This test measures the effective table, + normalizes it (to remove some inherent randomness of certain identifiers + and make it uniform regardless of particular names of block devices, snap + revisions, etc.) and then compares it to a canned copy. + + There are several reference tables, one for core (aka all-snap system) and + one for classic. At this time only classic systems are measured and tested. + The classic systems are further divided into those using the core snap and + those using the older ubuntu-core snap. Lastly, they are divided by + architectures to take account any architecture specific differences. +prepare: | + echo "Having installed a busybox" + snap install snapd-hacker-toolbelt +execute: | + echo "We can map the kernel architecture name to snap architecture name" + arch=$(./snap-arch.py) + echo "We can run busybox true so that snap-confine creates a mount namespace" + snapd-hacker-toolbelt.busybox true + echo "Using nsenter we can move to that namespace, inspect and normalize the mount table" + nsenter -m/run/snapd/ns/snapd-hacker-toolbelt.mnt \ + cat /proc/self/mountinfo | ./process.py > observed.json + echo "We can now compare the obtained mount table to expected values" + if [ -e /snap/core/current ]; then + cmp observed.json expected.classic.core.$SPREAD_BACKEND.$arch.json + else + cmp observed.json expected.classic.ubuntu-core.$SPREAD_BACKEND.$arch.json + fi +debug: | + echo "When something goes wrong we can display a human-readable diff" + arch=$(./snap-arch.py) + if [ -e /snap/core/current ]; then + diff -u observed.json expected.classic.core.$SPREAD_BACKEND.$arch.json || : + else + diff -u observed.json expected.classic.ubuntu-core.$SPREAD_BACKEND.$arch.json || : + fi + echo "And pastebin the raw table for analysis" + apt-get install pastebinit + nsenter -m/run/snapd/ns/snapd-hacker-toolbelt.mnt \ + cat /proc/self/mountinfo | pastebinit +restore: | + snap remove snapd-hacker-toolbelt + rm -f observed.json diff --git a/cmd/snap-confine/spread-tests/main/mount-ns-sharing/task.yaml b/cmd/snap-confine/spread-tests/main/mount-ns-sharing/task.yaml new file mode 100644 index 00000000..402e9ed9 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-ns-sharing/task.yaml @@ -0,0 +1,20 @@ +summary: mount namespace is shared among processes +details: | + The mount namespace is automatically shared amongst processes belonging to + a given snap. The namespace is preserved until the machine reboots or until + it is discarded with snap-discard-ns. +prepare: | + # NOTE: devmode is required because otherwise we cannot read /proc/self/ns/mnt + snap install --devmode snapd-hacker-toolbelt +execute: | + export PATH=/snap/bin:$PATH + echo "The mount namespace inside a snap is different" + outer_mnt_ns=$(readlink /proc/self/ns/mnt) + inner_mnt_ns=$(snapd-hacker-toolbelt.busybox readlink /proc/self/ns/mnt) + [ "$outer_mnt_ns" != "$inner_mnt_ns" ] + echo "The mount namespace is stable across invocations" + for i in $(seq 100); do + [ "$inner_mnt_ns" = "$(snapd-hacker-toolbelt.busybox readlink /proc/self/ns/mnt)" ] + done +restore: | + snap remove snapd-hacker-toolbelt diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-destination/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-destination/task.yaml new file mode 100644 index 00000000..00727918 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-destination/task.yaml @@ -0,0 +1,26 @@ +summary: Apparmor profile prevents bind-mounting to /snap/bin +# This is blacklisted on debian because it relies on apparmor mount mediation +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create bind mount /snap/bin somewhere" + echo "/snap/snapd-hacker-toolbelt/mnt -> /snap/bin" + mkdir -p /var/lib/snapd/mount + echo "/snap/snapd-hacker-toolbelt/current/mnt /snap/bin none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + cd / + echo "Let's clear the kernel ring buffer" + dmesg -c + echo "We can now run busybox true and expect it to fail" + orig_ratelimit=$(sysctl -n kernel.printk_ratelimit) + sysctl -w kernel.printk_ratelimit=0 + ! /snap/bin/snapd-hacker-toolbelt.busybox true + sysctl -w kernel.printk_ratelimit=$orig_ratelimit + echo "Not only the command failed because snap-confine failed, we see why!" + dmesg --ctime | grep 'apparmor="DENIED" operation="mount" info="failed srcname match" error=-13 profile="/usr/lib/snapd/snap-confine" name="/snap/bin/" pid=[0-9]\+ comm="ubuntu-core-lau" srcname="/snap/snapd-hacker-toolbelt/[0-9]\+/mnt/" flags="rw, bind"' +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + dmesg -c diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-source/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-source/task.yaml new file mode 100644 index 00000000..4eb3a66c --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-bin-snap-source/task.yaml @@ -0,0 +1,26 @@ +summary: Apparmor profile prevents bind-mounting from /snap/bin +# This is blacklisted on debian because it relies on apparmor mount mediation +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create bind mount /snap/bin somewhere" + echo "/snap/bin -> /snap/snapd-hacker-toolbelt/mnt" + mkdir -p /var/lib/snapd/mount + echo "/snap/bin /snap/snapd-hacker-toolbelt/current/mnt none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + cd / + echo "Let's clear the kernel ring buffer" + dmesg -c + echo "We can now run busybox true and expect it to fail" + orig_ratelimit=$(sysctl -n kernel.printk_ratelimit) + sysctl -w kernel.printk_ratelimit=0 + ! /snap/bin/snapd-hacker-toolbelt.busybox true + sysctl -w kernel.printk_ratelimit=$orig_ratelimit + echo "Not only the command failed because snap-confine failed, we see why!" + dmesg --ctime | grep 'apparmor="DENIED" operation="mount" info="failed srcname match" error=-13 profile="/usr/lib/snapd/snap-confine" name="/snap/snapd-hacker-toolbelt/[0-9]\+/mnt/" pid=[0-9]\+ comm="ubuntu-core-lau" srcname="/snap/bin/" flags="rw, bind"' +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + dmesg -c diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-missing-dst/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-missing-dst/task.yaml new file mode 100644 index 00000000..5ecc9fac --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-missing-dst/task.yaml @@ -0,0 +1,19 @@ +summary: Check that missing destination directory aborts mount processing +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create a read-only bind-mount" + echo "/var/snap/snapd-hacker-toolbelt/common/src -> /var/snap/snapd-hacker-toolbelt/common/dst" + mkdir -p /var/lib/snapd/mount + echo "/var/snap/snapd-hacker-toolbelt/common/src /var/snap/snapd-hacker-toolbelt/common/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + echo "We can now create the source directory, missing the destination directory" + mkdir -p /var/snap/snapd-hacker-toolbelt/common/src +execute: | + echo "We can now run busybox.true and expect it to fail" + ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true ) +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-missing-src/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-missing-src/task.yaml new file mode 100644 index 00000000..70dbf535 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-missing-src/task.yaml @@ -0,0 +1,19 @@ +summary: Check that missing source directory aborts mount processing +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create a read-only bind-mount" + echo "/var/snap/snapd-hacker-toolbelt/common/src -> /var/snap/snapd-hacker-toolbelt/common/dst" + mkdir -p /var/lib/snapd/mount + echo "/var/snap/snapd-hacker-toolbelt/common/src /var/snap/snapd-hacker-toolbelt/common/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + echo "We can now create the destination directory, missing the source directory" + mkdir -p /var/snap/snapd-hacker-toolbelt/common/dst +execute: | + echo "We can now run busybox.true and expect it to fail" + ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true ) +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-mount-tmpfs/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-mount-tmpfs/task.yaml new file mode 100644 index 00000000..8226b412 --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-mount-tmpfs/task.yaml @@ -0,0 +1,20 @@ +summary: Check that mount profiles cannot be used to mount tmpfs +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap list | grep -q snapd-hacker-toolbelt || snap install snapd-hacker-toolbelt + + echo "We can change its mount profile externally to mount tmpfs at /var/snap/snapd-hacker-toolbelt/mnt" + mkdir -p /var/lib/snapd/mount + echo "none /var/snap/snapd-hacker-toolbelt/common/mnt tmpfs rw 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab + + echo "We can now create the test mount directory" + mkdir -p /var/snap/snapd-hacker-toolbelt/common/mnt + + echo "We can now run busybox.true and expect it to fail" + ( cd / && ! /snap/bin/snapd-hacker-toolbelt.busybox true ) diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-ro-mount/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-ro-mount/task.yaml new file mode 100644 index 00000000..2ae72e6a --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-ro-mount/task.yaml @@ -0,0 +1,18 @@ +summary: Check that read-only bind mounts can be created +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can change its mount profile externally to create a read-only bind-mount" + echo "/snap/snapd-hacker-toolbelt/current/src -> /snap/snapd-hacker-toolbelt/current/dst" + mkdir -p /var/lib/snapd/mount + echo "/snap/snapd-hacker-toolbelt/current/src /snap/snapd-hacker-toolbelt/current/dst none bind,ro 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + cd / + echo "We can now look at the .id file in the destination directory" + [ "$(/snap/bin/snapd-hacker-toolbelt.busybox cat /snap/snapd-hacker-toolbelt/current/dst/.id)" = "source" ] +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab diff --git a/cmd/snap-confine/spread-tests/main/mount-profiles-rw-mount/task.yaml b/cmd/snap-confine/spread-tests/main/mount-profiles-rw-mount/task.yaml new file mode 100644 index 00000000..b2e23ece --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-profiles-rw-mount/task.yaml @@ -0,0 +1,24 @@ +summary: Check that read-write bind mounts can be created +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "We can connect it to the mount-observe slot from the core" + snap connect snapd-hacker-toolbelt:mount-observe ubuntu-core:mount-observe + echo "We can change its mount profile externally to create a read-only bind-mount" + echo "/snap/snapd-hacker-toolbelt/current/src -> /snap/snapd-hacker-toolbelt/current/dst" + mkdir -p /var/lib/snapd/mount + echo "/snap/snapd-hacker-toolbelt/current/src /snap/snapd-hacker-toolbelt/current/dst none bind,rw 0 0" > /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab +execute: | + cd / + echo "We can now look at the .id file in the destination directory" + [ "$(/snap/bin/snapd-hacker-toolbelt.busybox cat /snap/snapd-hacker-toolbelt/current/dst/.id)" = "source" ] + echo "As well as the current mount points" + # FIXME: this doesn't show 'rw', bind mounts confuse most tools and it + # seems that busybox is not any different here. + /snap/bin/snapd-hacker-toolbelt.busybox mount | grep snapd-hacker-toolbelt +restore: | + snap remove snapd-hacker-toolbelt + rm -rf /var/snap/snapd-hacker-toolbelt + rm -f /var/lib/snapd/mount/snap.snapd-hacker-toolbelt.busybox.fstab diff --git a/cmd/snap-confine/spread-tests/main/mount-usr-src/task.yaml b/cmd/snap-confine/spread-tests/main/mount-usr-src/task.yaml new file mode 100644 index 00000000..98a7b4fd --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/mount-usr-src/task.yaml @@ -0,0 +1,17 @@ +summary: Check for https://bugs.launchpad.net/snap-confine/+bug/1597842 +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + The snappy execution environment should contain the /usr/src directory + from the host filesystem when running on a classic distribution. +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "and having connected the mount-observe interface" + snap connect snapd-hacker-toolbelt:mount-observe ubuntu-core:mount-observe +execute: | + cd / + echo "We can ensure that /usr/src is mounted" + /snap/bin/snapd-hacker-toolbelt.busybox cat /proc/self/mounts | grep ' /usr/src ' +restore: | + snap remove snapd-hacker-toolbelt diff --git a/cmd/snap-confine/spread-tests/main/test-seccomp-compat/task.yaml b/cmd/snap-confine/spread-tests/main/test-seccomp-compat/task.yaml new file mode 100644 index 00000000..5a5c189e --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/test-seccomp-compat/task.yaml @@ -0,0 +1,16 @@ +summary: Check that basic install works +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +# +# This test only makes sense on x86_64 as it can execute i386 code in addition +# to native x86_64 code). +systems: [-debian-8, -ubuntu-16.04-32] +prepare: | + snap install --edge test-seccomp-compat +execute: | + cd / + echo Run the 64 bit binary + test-seccomp-compat.true64 + echo Run the 32 bit binary + test-seccomp-compat.true32 +restore: | + snap remove test-seccomp-compat diff --git a/cmd/snap-confine/spread-tests/main/test-snap-runs/task.yaml b/cmd/snap-confine/spread-tests/main/test-snap-runs/task.yaml new file mode 100644 index 00000000..857c863c --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/test-snap-runs/task.yaml @@ -0,0 +1,15 @@ +summary: Check that basic install works +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +prepare: | + snap install snapd-hacker-toolbelt +execute: | + cd / + echo Run some hello-world stuff + snapd-hacker-toolbelt.busybox echo "Hello World" | grep Hello + snapd-hacker-toolbelt.busybox env | grep SNAP_NAME=snapd-hacker-toolbelt + echo Ensure that we get an error if we try to abuse the sandbox + if snapd-hacker-toolbelt.busybox touch /var/tmp/evil; then exit 1; fi + dmesg -c +restore: | + snap remove snapd-hacker-toolbelt diff --git a/cmd/snap-confine/spread-tests/main/ubuntu-core-launcher-exists/task.yaml b/cmd/snap-confine/spread-tests/main/ubuntu-core-launcher-exists/task.yaml new file mode 100644 index 00000000..236b91de --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/ubuntu-core-launcher-exists/task.yaml @@ -0,0 +1,6 @@ +summary: Check that ubuntu-core-launcher executes correctly +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +execute: | + echo "ubuntu-core-launcher is installed and responds to --help" + ubuntu-core-launcher --help 2>&1 | grep -F -q 'Usage: ubuntu-core-launcher ' diff --git a/cmd/snap-confine/spread-tests/main/user-data-dir-created/task.yaml b/cmd/snap-confine/spread-tests/main/user-data-dir-created/task.yaml new file mode 100644 index 00000000..dc2b69aa --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/user-data-dir-created/task.yaml @@ -0,0 +1,23 @@ +summary: Ensure that SNAP_USER_DATA directory is created by snap-confine +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + A regression was found in snap-confine where the new code path in snapd was + not active yet but the corresponding code path in snap-confine was already + removed. This resulted in the $SNAP_USER_DATA directory not being created + at runtime. + This test checks that it is actually created +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "Having removed the SNAP_USER_DATA directory" + rm -rf "$HOME/snap/snapd-hacker-toolbelt/" +execute: | + cd / + echo "We can now run snapd-hacker-toolbelt.busybox true" + /snap/bin/snapd-hacker-toolbelt.busybox true + echo "And see that the SNAP_USER_DATA directory was created" + test -d $HOME/snap/snapd-hacker-toolbelt +restore: | + snap remove snapd-hacker-toolbelt + rm -rf "$HOME/snap/snapd-hacker-toolbelt/" diff --git a/cmd/snap-confine/spread-tests/main/user-xdg-runtime-dir-created/task.yaml b/cmd/snap-confine/spread-tests/main/user-xdg-runtime-dir-created/task.yaml new file mode 100644 index 00000000..b15d7bdd --- /dev/null +++ b/cmd/snap-confine/spread-tests/main/user-xdg-runtime-dir-created/task.yaml @@ -0,0 +1,21 @@ +summary: Ensure that XDG_RUNTIME_DIR directory is created by snap-confine +# This is blacklisted on debian because debian doesn't use apparmor yet +systems: [-debian-8] +details: | + This test checks that XDG_RUNTIME_DIR is actually created +prepare: | + echo "Having installed the snapd-hacker-toolbelt snap" + snap install snapd-hacker-toolbelt + echo "Having removed the XDG_RUNTIME_DIR directory" + rm -rf "/run/user/`id -u`/snapd-hacker-toolbelt" +execute: | + cd / + echo "FIXME: export XDG_RUNTIME_DIR for now until snapd does it" + export XDG_RUNTIME_DIR="/run/user/`id -u`/snapd-hacker-toolbelt" + echo "We can now run snapd-hacker-toolbelt.busybox true" + /snap/bin/snapd-hacker-toolbelt.busybox true + echo "And see that the XDG_RUNTIME_DIR directory was created" + test -d /run/user/`id -u`/snapd-hacker-toolbelt +restore: | + snap remove snapd-hacker-toolbelt + rm -rf "/run/user/`id -u`/snapd-hacker-toolbelt" diff --git a/cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml b/cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml new file mode 100644 index 00000000..52a7fd09 --- /dev/null +++ b/cmd/snap-confine/spread-tests/regression/lp-1599608/task.yaml @@ -0,0 +1,69 @@ +summary: Check that execle doesn't regress +# This is blacklisted on debian because we first have to get the dpkg-vendor patches +systems: [-debian-8] +details: | + The setup for this test is unorthodox because by the time the cgroup code is + executed, the mounts are in place and /lib/udev/snappy-app-dev from the core + snap is used. Unfortunately, simple bind mounts over + /snap/ubuntu-core/current/lib/udev don't work and the core snap must be + unpacked, lib/udev/snappy-app-dev modified to be tested, repacked and mounted. + We unmount the core snap and move it aside to avoid both the original and the + updated core snap from being mounted on the same mount point, which confuses + the kernel. +prepare: | + echo "This test is disabled because it causes failures for subsequent tests" + echo "it seems to unmount ubuntu-core snap and not re-mount the original one correctly" + exit 0 + cd / + echo "Install hello-world" + snap install hello-world + systemctl stop snapd.refresh.timer snapd.service snapd.socket + # all of this ls madness can go away when we have remote environment + # variables + echo "Unmount original core snap" + umount $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1) + mv $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1).orig + echo "Create modified core snap for snappy-app-dev" + unsquashfs $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1) + echo 'echo PATH=$PATH > /run/udev/spread-test.out' >> ./squashfs-root/lib/udev/snappy-app-dev + echo 'echo TESTVAR=$TESTVAR >> /run/udev/spread-test.out' >> ./squashfs-root/lib/udev/snappy-app-dev + mksquashfs ./squashfs-root $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1 | sed 's/.orig//') -comp xz + if [ ! -e $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) ]; then exit 1; fi + echo "Mount modified core snap" + mount $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1) + systemctl start snapd.refresh.timer snapd.service snapd.socket +execute: | + exit 0 + cd / + echo "Add a udev tag so affected code branch is exercised" + echo 'KERNEL=="uinput", TAG+="snap_hello-world_env"' > /etc/udev/rules.d/70-spread-test.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + PATH=/foo:$PATH TESTVAR=bar hello-world.env | grep PATH + cat /run/udev/spread-test.out + echo "Ensure user-specified PATH is not used" + ! grep 'PATH=/foo' /run/udev/spread-test.out + echo "Ensure environment is clean" + ! grep 'TESTVAR=bar' /run/udev/spread-test.out +restore: | + exit 0 + echo "Remove hello-world" + snap remove hello-world + systemctl stop snapd.refresh.timer snapd.service snapd.socket + echo "Unmount the modified core snap" + # all of this ls madness can go away when we have remote environment + # variables + umount $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1) + if [ "x"$(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1) != "x" ]; then mv -f $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1) $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap.orig | tail -1 | sed 's/.orig//') ; fi + echo "Mount the original core snap" + mount $(ls -1 /var/lib/snapd/snaps/ubuntu-core_*.snap | tail -1) $(ls -1d /snap/ubuntu-core/* | grep -v current | tail -1) + rm -rf /squashfs-root + rm -f /run/udev/spread-test.out + rm -f /etc/udev/rules.d/70-spread-test.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + systemctl start snapd.refresh.timer snapd.service snapd.socket diff --git a/cmd/snap-confine/spread-tests/release.sh b/cmd/snap-confine/spread-tests/release.sh new file mode 100755 index 00000000..803ae74e --- /dev/null +++ b/cmd/snap-confine/spread-tests/release.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# This script creates a new release tarball +set -xue + +# Sanity check, are we in the top-level directory of the tree? +test -f configure.ac || ( echo 'this script must be executed from the top-level of the tree' && exit 1) + +# Record where the top level directory is +top_dir=$(pwd) + +# Create source distribution tarball and place it in the top-level directory. +create_dist_tarball() { + # Load the version number from a dedicated file + local pkg_version= + pkg_version="$(cat "$top_dir/VERSION")" + + # Ensure that build system is up-to-date and ready + autoreconf -f -i + # XXX: This fixes somewhat odd error when configure below (in an empty directory) fails with: + # configure: error: source directory already configured; run "make distclean" there first + test -f Makefile && make distclean + + # Create a scratch space to run configure + scratch_dir="$(mktemp -d)" + trap 'rm -rf "$scratch_dir"' EXIT + + # Configure the project in a scratch directory + cd "$scratch_dir" + "$top_dir/configure" --prefix=/usr + + # Create the distribution tarball + make dist + + # Ensure we got the tarball we were expecting to see + test -f "snap-confine-$pkg_version.tar.gz" + + # Move it to the top-level directory + mv "snap-confine-$pkg_version.tar.gz" "$top_dir/" +} + +create_dist_tarball diff --git a/cmd/snap-confine/spread-tests/spread-prepare.sh b/cmd/snap-confine/spread-tests/spread-prepare.sh new file mode 100755 index 00000000..a547b0d6 --- /dev/null +++ b/cmd/snap-confine/spread-tests/spread-prepare.sh @@ -0,0 +1,179 @@ +#!/bin/bash +# This script is started by spread to prepare the execution environment +set -xue + +# Sanity check, are we in the top-level directory of the tree? +test -f configure.ac || ( echo 'this script must be executed from the top-level of the tree' && exit 1) + +# Record where the top level directory is +top_dir=$(pwd) + +# Record the current distribution release data to know what to do +# shellcheck disable=SC1091 +{ + release_ID="$( . /etc/os-release && echo "${ID:-linux}" )" + release_VERSION_ID="$( . /etc/os-release && echo "${VERSION_ID:-}" )" +} + + +build_debian_or_ubuntu_package() { + local pkg_version + local distro_packaging_git_branch + local distro_packaging_git + local distro_archive + local distro_codename + local sbuild_createchroot_extra="" + pkg_version="$(cat "$top_dir/VERSION")" + + if [ ! -f "$top_dir/spread-tests/distros/$release_ID.$release_VERSION_ID" ] || \ + [ ! -f "$top_dir/spread-tests/distros/$release_ID.common" ]; then + echo "Distribution: $release_ID (release $release_VERSION_ID) is not supported" + echo "please read this script and create new files in spread-test/distros" + exit 1 + fi + + # source the distro specific vars + # shellcheck disable=SC1090 + { + . "$top_dir/spread-tests/distros/$release_ID.$release_VERSION_ID" + . "$top_dir/spread-tests/distros/$release_ID.common" + } + + # sanity check, ensure that essential variables were defined + test -n "$distro_packaging_git_branch" + test -n "$distro_packaging_git" + test -n "$distro_archive" + test -n "$distro_codename" + + # Create a scratch space + scratch_dir="$(mktemp -d)" + trap 'rm -rf "$scratch_dir"' EXIT + + # Do everything in the scratch directory + cd "$scratch_dir" + + # Fetch the current Ubuntu packaging for the appropriate release + git clone -b "$distro_packaging_git_branch" "$distro_packaging_git" distro-packaging + + # Install all the build dependencies declared by the package. + apt-get install --quiet -y gdebi-core + gdebi --quiet --apt-line ./distro-packaging/debian/control | xargs -r apt-get install --quiet -y + + # Generate a new upstream tarball from the current state of the tree + ( cd "$top_dir" && spread-tests/release.sh ) + + # Prepare the .orig tarball and unpackaged source tree + cp "$top_dir/snap-confine-$pkg_version.tar.gz" "snap-confine_$pkg_version.orig.tar.gz" + tar -zxf "snap-confine_$pkg_version.orig.tar.gz" + + # Apply the debian directory from downstream packaging to form a complete source package + mv "distro-packaging/debian" "snap-confine-$pkg_version/debian" + rm -rf distro-packaging + + # Add an automatically-generated changelog entry + # The --controlmaint takes the maintainer details from debian/control + ( cd "snap-confine-$pkg_version" && dch --controlmaint --newversion "${pkg_version}-1" "Automatic CI build") + + # Build an unsigned source package + ( cd "snap-confine-$pkg_version" && dpkg-buildpackage -uc -us -S ) + + # Copy source package files to the top-level directory (this helps for + # interactive debugging since the package is available right there) + cp ./*.dsc ./*.debian.tar.* ./*.orig.tar.gz "$top_dir/" + + # Ensure that we have a sbuild chroot ready + if ! schroot -l | grep "chroot:${distro_codename}-.*-sbuild"; then + sbuild-createchroot \ + --include=eatmydata \ + "--make-sbuild-tarball=/var/lib/sbuild/${distro_codename}-amd64.tar.gz" \ + "$sbuild_createchroot_extra" \ + "$distro_codename" "$(mktemp -d)" \ + "$distro_archive" + fi + + # Build a binary package in a clean chroot. + # NOTE: nocheck is because the package still includes old unit tests that + # are deeply integrated into how ubuntu apparmor denials are logged. This + # should be removed once those test are migrated to spread testes. + DEB_BUILD_OPTIONS=nocheck sbuild \ + --arch-all \ + --dist="$distro_codename" \ + --batch \ + "snap-confine_${pkg_version}-1.dsc" + + # Copy all binary packages to the top-level directory + cp ./*.deb "$top_dir/" +} + + +# Apply tweaks +case "$release_ID" in + ubuntu) + # apt update is hanging on security.ubuntu.com with IPv6. + sysctl -w net.ipv6.conf.all.disable_ipv6=1 + trap "sysctl -w net.ipv6.conf.all.disable_ipv6=0" EXIT + ;; +esac + +# Install all the build dependencies +case "$release_ID" in + ubuntu|debian) + # treat APT_PROXY as a location of apt-cacher-ng to use + if [ -n "${APT_PROXY:-}" ]; then + printf 'Acquire::http::Proxy "%s";\n' "$APT_PROXY" > /etc/apt/apt.conf.d/00proxy + fi + # cope with unexpected /etc/apt/apt.conf.d/95cloud-init-proxy that may be in the image + rm -f /etc/apt/apt.conf.d/95cloud-init-proxy || : + # trusty support is under development right now + # we special-case the release until we have officially landed + if [ "$release_ID" = "ubuntu" ] && [ "$release_VERSION_ID" = "14.04" ]; then + add-apt-repository ppa:thomas-voss/trusty + fi + apt-get update + apt-get dist-upgrade -y + if [ "$release_ID" = "ubuntu" ] && [ "$release_VERSION_ID" = "14.04" ]; then + apt-get install -y systemd + # starting systemd manually is working around + # systemd not running as PID 1 on trusty systems. + service systemd start + fi + # On Debian and derivatives we need the following things: + # - sbuild -- to build the binary package with extra hygiene + # - devscripts -- to modify the changelog automatically + # - git -- to clone native downstream packaging + apt-get install --quiet -y sbuild devscripts git + # XXX: Taken from https://wiki.debian.org/sbuild + mkdir -p /root/.gnupg + # NOTE: We cannot use sbuild-update --keygen as virtual machines lack + # the necessary entropy to generate keys before the spread timeout + # kicks in. Instead we just copy pre-made, insecure keys from the + # source repository. + mkdir -p /var/lib/sbuild/apt-keys/ + cp -a "$top_dir/spread-tests/data/apt-keys/"* /var/lib/sbuild/apt-keys/ + sbuild-adduser "$LOGNAME" + ;; + *) + echo "unsupported distribution: $release_ID" + echo "patch spread-prepare to teach it about how to install build dependencies" + exit 1 + ;; +esac + +# Build and install the native package using downstream packaging and the fresh upstream tarball +case "$release_ID" in + ubuntu|debian) + build_debian_or_ubuntu_package "$release_ID" "$release_VERSION_ID" + # Install the freshly-built packages + dpkg -i snap-confine_*.deb || apt-get -f install -y + dpkg -i ubuntu-core-launcher_*.deb || apt-get -f install -y + # Install snapd (testes require it) + apt-get install -y snapd + ;; + *) + echo "unsupported distribution: $release_ID" + exit 1 + ;; +esac + +# Install the core snap +snap list | grep -q ubuntu-core || snap install ubuntu-core diff --git a/cmd/snap-confine/udev-support.c b/cmd/snap-confine/udev-support.c new file mode 100644 index 00000000..e352b19d --- /dev/null +++ b/cmd/snap-confine/udev-support.c @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" +#include "udev-support.h" + +static void +_run_snappy_app_dev_add_majmin(struct snappy_udev *udev_s, + const char *path, unsigned major, unsigned minor) +{ + int status = 0; + pid_t pid = fork(); + if (pid < 0) { + die("cannot fork support process for device cgroup assignment"); + } + if (pid == 0) { + uid_t real_uid, effective_uid, saved_uid; + if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0) + die("cannot get real, effective and saved user IDs"); + // can't update the cgroup unless the real_uid is 0, euid as + // 0 is not enough + if (real_uid != 0 && effective_uid == 0) + if (setuid(0) != 0) + die("cannot set user ID to zero"); + char buf[64] = { 0 }; + // pass snappy-add-dev an empty environment so the + // user-controlled environment can't be used to subvert + // snappy-add-dev + char *env[] = { NULL }; + if (minor == UINT_MAX) { + sc_must_snprintf(buf, sizeof(buf), "%u:*", major); + } else { + sc_must_snprintf(buf, sizeof(buf), "%u:%u", major, + minor); + } + debug("running snappy-app-dev add %s %s %s", udev_s->tagname, + path, buf); + execle("/lib/udev/snappy-app-dev", "/lib/udev/snappy-app-dev", + "add", udev_s->tagname, path, buf, NULL, env); + die("execl failed"); + } + if (waitpid(pid, &status, 0) < 0) + die("waitpid failed"); + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) + die("child exited with status %i", WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + die("child died with signal %i", WTERMSIG(status)); +} + +void run_snappy_app_dev_add(struct snappy_udev *udev_s, const char *path) +{ + if (udev_s == NULL) + die("snappy_udev is NULL"); + if (udev_s->udev == NULL) + die("snappy_udev->udev is NULL"); + if (udev_s->tagname_len == 0 + || udev_s->tagname_len >= MAX_BUF + || strnlen(udev_s->tagname, MAX_BUF) != udev_s->tagname_len + || udev_s->tagname[udev_s->tagname_len] != '\0') + die("snappy_udev->tagname has invalid length"); + + debug("%s: %s %s", __func__, path, udev_s->tagname); + + struct udev_device *d = + udev_device_new_from_syspath(udev_s->udev, path); + if (d == NULL) + die("cannot find device from syspath %s", path); + dev_t devnum = udev_device_get_devnum(d); + udev_device_unref(d); + + unsigned major = MAJOR(devnum); + unsigned minor = MINOR(devnum); + _run_snappy_app_dev_add_majmin(udev_s, path, major, minor); +} + +/* + * snappy_udev_init() - setup the snappy_udev structure. Return 0 if devices + * are assigned, else return -1. Callers should use snappy_udev_cleanup() to + * cleanup. + */ +int snappy_udev_init(const char *security_tag, struct snappy_udev *udev_s) +{ + debug("%s", __func__); + int rc = 0; + + udev_s->tagname[0] = '\0'; + udev_s->tagname_len = 0; + // TAG+="snap_" (udev doesn't like '.' in the tag name) + udev_s->tagname_len = sc_must_snprintf(udev_s->tagname, MAX_BUF, + "%s", security_tag); + for (size_t i = 0; i < udev_s->tagname_len; i++) + if (udev_s->tagname[i] == '.') + udev_s->tagname[i] = '_'; + + udev_s->udev = udev_new(); + if (udev_s->udev == NULL) + die("udev_new failed"); + + udev_s->devices = udev_enumerate_new(udev_s->udev); + if (udev_s->devices == NULL) + die("udev_enumerate_new failed"); + + if (udev_enumerate_add_match_tag(udev_s->devices, udev_s->tagname) != 0) + die("udev_enumerate_add_match_tag"); + + if (udev_enumerate_scan_devices(udev_s->devices) != 0) + die("udev_enumerate_scan failed"); + + udev_s->assigned = udev_enumerate_get_list_entry(udev_s->devices); + if (udev_s->assigned == NULL) + rc = -1; + + return rc; +} + +void snappy_udev_cleanup(struct snappy_udev *udev_s) +{ + // udev_s->assigned does not need to be unreferenced since it is a + // pointer into udev_s->devices + if (udev_s->devices != NULL) + udev_enumerate_unref(udev_s->devices); + if (udev_s->udev != NULL) + udev_unref(udev_s->udev); +} + +void setup_devices_cgroup(const char *security_tag, struct snappy_udev *udev_s) +{ + debug("%s", __func__); + // Devices that must always be present + const char *static_devices[] = { + "/sys/class/mem/null", + "/sys/class/mem/full", + "/sys/class/mem/zero", + "/sys/class/mem/random", + "/sys/class/mem/urandom", + "/sys/class/tty/tty", + "/sys/class/tty/console", + "/sys/class/tty/ptmx", + NULL, + }; + + if (udev_s == NULL) + die("snappy_udev is NULL"); + if (udev_s->udev == NULL) + die("snappy_udev->udev is NULL"); + if (udev_s->devices == NULL) + die("snappy_udev->devices is NULL"); + if (udev_s->assigned == NULL) + die("snappy_udev->assigned is NULL"); + if (udev_s->tagname_len == 0 + || udev_s->tagname_len >= MAX_BUF + || strnlen(udev_s->tagname, MAX_BUF) != udev_s->tagname_len + || udev_s->tagname[udev_s->tagname_len] != '\0') + die("snappy_udev->tagname has invalid length"); + + // create devices cgroup controller + char cgroup_dir[PATH_MAX] = { 0 }; + + sc_must_snprintf(cgroup_dir, sizeof(cgroup_dir), + "/sys/fs/cgroup/devices/%s/", security_tag); + + if (mkdir(cgroup_dir, 0755) < 0 && errno != EEXIST) + die("cannot create cgroup hierarchy %s", cgroup_dir); + + // move ourselves into it + char cgroup_file[PATH_MAX] = { 0 }; + sc_must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, + "tasks"); + + char buf[128] = { 0 }; + sc_must_snprintf(buf, sizeof(buf), "%i", getpid()); + write_string_to_file(cgroup_file, buf); + + // deny by default. Write 'a' to devices.deny to remove all existing + // devices that were added in previous launcher invocations, then add + // the static and assigned devices. This ensures that at application + // launch the cgroup only has what is currently assigned. + sc_must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, + "devices.deny"); + write_string_to_file(cgroup_file, "a"); + + // add the common devices + for (int i = 0; static_devices[i] != NULL; i++) + run_snappy_app_dev_add(udev_s, static_devices[i]); + + // add glob for current and future PTY slaves. We unconditionally add + // them since we use a devpts newinstance. Unix98 PTY slaves major + // are 136-143. + // https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/devices.txt + for (unsigned pty_major = 136; pty_major <= 143; pty_major++) { + // '/dev/pts/slaves' is only used for debugging and by + // /lib/udev/snappy-app-dev to determine if it is a block + // device, so just use something to indicate what the + // addition is for + _run_snappy_app_dev_add_majmin(udev_s, "/dev/pts/slaves", + pty_major, UINT_MAX); + } + + // nvidia modules are proprietary and therefore aren't in sysfs and + // can't be udev tagged. For now, just add existing nvidia devices to + // the cgroup unconditionally (AppArmor will still mediate the access). + // We'll want to rethink this if snapd needs to mediate access to other + // proprietary devices. + // + // Device major and minor numbers are described in (though nvidia-uvm + // currently isn't listed): + // https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/devices.txt + char nv_path[15] = { 0 }; // /dev/nvidiaXXX + const char *nvctl_path = "/dev/nvidiactl"; + const char *nvuvm_path = "/dev/nvidia-uvm"; + const char *nvidia_modeset_path = "/dev/nvidia-modeset"; + + struct stat sbuf; + + // /dev/nvidia0 through /dev/nvidia254 + for (unsigned nv_minor = 0; nv_minor < 255; nv_minor++) { + sc_must_snprintf(nv_path, sizeof(nv_path), "/dev/nvidia%u", + nv_minor); + + // Stop trying to find devices after one is not found. In this + // manner, we'll add /dev/nvidia0 and /dev/nvidia1 but stop + // trying to find nvidia3 - nvidia254 if nvidia2 is not found. + if (stat(nv_path, &sbuf) != 0) { + break; + } + _run_snappy_app_dev_add_majmin(udev_s, nv_path, + MAJOR(sbuf.st_rdev), + MINOR(sbuf.st_rdev)); + } + + // /dev/nvidiactl + if (stat(nvctl_path, &sbuf) == 0) { + _run_snappy_app_dev_add_majmin(udev_s, nvctl_path, + MAJOR(sbuf.st_rdev), + MINOR(sbuf.st_rdev)); + } + // /dev/nvidia-uvm + if (stat(nvuvm_path, &sbuf) == 0) { + _run_snappy_app_dev_add_majmin(udev_s, nvuvm_path, + MAJOR(sbuf.st_rdev), + MINOR(sbuf.st_rdev)); + } + // /dev/nvidia-modeset + if (stat(nvidia_modeset_path, &sbuf) == 0) { + _run_snappy_app_dev_add_majmin(udev_s, nvidia_modeset_path, + MAJOR(sbuf.st_rdev), + MINOR(sbuf.st_rdev)); + } + // /dev/uhid isn't represented in sysfs, so add it to the device cgroup + // if it exists and let AppArmor handle the mediation + if (stat("/dev/uhid", &sbuf) == 0) { + _run_snappy_app_dev_add_majmin(udev_s, "/dev/uhid", + MAJOR(sbuf.st_rdev), + MINOR(sbuf.st_rdev)); + } + // add the assigned devices + while (udev_s->assigned != NULL) { + const char *path = udev_list_entry_get_name(udev_s->assigned); + if (path == NULL) + die("udev_list_entry_get_name failed"); + run_snappy_app_dev_add(udev_s, path); + udev_s->assigned = udev_list_entry_get_next(udev_s->assigned); + } +} diff --git a/cmd/snap-confine/udev-support.h b/cmd/snap-confine/udev-support.h new file mode 100644 index 00000000..6ad59ae4 --- /dev/null +++ b/cmd/snap-confine/udev-support.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_UDEV_SUPPORT_H +#define SNAP_CONFINE_UDEV_SUPPORT_H + +#include + +#include + +#define MAX_BUF 1000 + +struct snappy_udev { + struct udev *udev; + struct udev_enumerate *devices; + struct udev_list_entry *assigned; + char tagname[MAX_BUF]; + size_t tagname_len; +}; + +void run_snappy_app_dev_add(struct snappy_udev *udev_s, const char *path); +int snappy_udev_init(const char *security_tag, struct snappy_udev *udev_s); +void snappy_udev_cleanup(struct snappy_udev *udev_s); +void setup_devices_cgroup(const char *security_tag, struct snappy_udev *udev_s); + +#endif diff --git a/cmd/snap-confine/user-support.c b/cmd/snap-confine/user-support.c new file mode 100644 index 00000000..fee292ed --- /dev/null +++ b/cmd/snap-confine/user-support.c @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ +#include "config.h" +#include "user-support.h" + +#include +#include +#include + +#include "../libsnap-confine-private/utils.h" + +void setup_user_data(void) +{ + const char *user_data = getenv("SNAP_USER_DATA"); + + if (user_data == NULL) + return; + + // Only support absolute paths. + if (user_data[0] != '/') { + die("user data directory must be an absolute path"); + } + + debug("creating user data directory: %s", user_data); + if (sc_nonfatal_mkpath(user_data, 0755) < 0) { + die("cannot create user data directory: %s", user_data); + }; +} + +void setup_user_xdg_runtime_dir(void) +{ + const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); + + if (xdg_runtime_dir == NULL) + return; + // Only support absolute paths. + if (xdg_runtime_dir[0] != '/') { + die("XDG_RUNTIME_DIR must be an absolute path"); + } + + errno = 0; + debug("creating user XDG_RUNTIME_DIR directory: %s", xdg_runtime_dir); + if (sc_nonfatal_mkpath(xdg_runtime_dir, 0755) < 0) { + die("cannot create user XDG_RUNTIME_DIR directory: %s", + xdg_runtime_dir); + } + // if successfully created the directory (ie, not EEXIST), then chmod it. + if (errno == 0 && chmod(xdg_runtime_dir, 0700) != 0) { + die("cannot change permissions of user XDG_RUNTIME_DIR directory to 0700"); + } +} diff --git a/cmd/snap-confine/user-support.h b/cmd/snap-confine/user-support.h new file mode 100644 index 00000000..859d04dd --- /dev/null +++ b/cmd/snap-confine/user-support.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAP_CONFINE_USER_SUPPORT_H +#define SNAP_CONFINE_USER_SUPPORT_H + +void setup_user_data(void); +void setup_user_xdg_runtime_dir(void); +void mkpath(const char *const path); + +#endif diff --git a/cmd/snap-discard-ns/snap-discard-ns.c b/cmd/snap-discard-ns/snap-discard-ns.c new file mode 100644 index 00000000..23ac9a59 --- /dev/null +++ b/cmd/snap-discard-ns/snap-discard-ns.c @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include +#include +#include + +#include "../libsnap-confine-private/locking.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" +#include "../snap-confine/ns-support.h" + +int main(int argc, char **argv) +{ + if (argc != 2) + die("Usage: %s snap-name", argv[0]); + const char *snap_name = argv[1]; + + int snap_lock_fd = sc_lock(snap_name); + debug("initializing mount namespace: %s", snap_name); + struct sc_ns_group *group = + sc_open_ns_group(snap_name, SC_NS_FAIL_GRACEFULLY); + if (group != NULL) { + sc_discard_preserved_ns_group(group); + sc_close_ns_group(group); + } + // Unlink the current mount profile, if any. + char profile_path[PATH_MAX] = { 0 }; + sc_must_snprintf(profile_path, sizeof(profile_path), + "/run/snapd/ns/snap.%s.fstab", snap_name); + if (unlink(profile_path) < 0) { + // Silently ignore ENOENT as the profile doens't have to be there. + if (errno != ENOENT) { + die("cannot remove current mount profile: %s", + profile_path); + } + } + + sc_unlock(snap_name, snap_lock_fd); + return 0; +} diff --git a/cmd/snap-discard-ns/snap-discard-ns.rst b/cmd/snap-discard-ns/snap-discard-ns.rst new file mode 100644 index 00000000..6e470bc8 --- /dev/null +++ b/cmd/snap-discard-ns/snap-discard-ns.rst @@ -0,0 +1,53 @@ +================ + snap-discard-ns +================ + +------------------------------------------------------------------------ +internal tool for discarding preserved namespaces of snappy applications +------------------------------------------------------------------------ + +:Author: zygmunt.krynicki@canonical.com +:Date: 2016-10-05 +:Copyright: Canonical Ltd. +:Version: 1.0.43 +:Manual section: 5 +:Manual group: snappy + +SYNOPSIS +======== + + snap-discard-ns SNAP_NAME + +DESCRIPTION +=========== + +The `snap-discard-ns` is a program used internally by `snapd` to discard a preserved +mount namespace of a particular snap. + +OPTIONS +======= + +The `snap-discard-ns` program does not support any options. + +ENVIRONMENT +=========== + +`snap-discard-ns` responds to the following environment variables + +`SNAP_CONFINE_DEBUG`: + When defined the program will print additional diagnostic information about + the actions being performed. All the output goes to stderr. + +FILES +===== + +`snap-discard-ns` uses the following files: + +`/run/snapd/ns/$SNAP_NAME.mnt`: + + The preserved mount namespace that is unmounted by `snap-discard-ns`. + +BUGS +==== + +Please report all bugs with https://bugs.launchpad.net/snap-confine/+filebug diff --git a/cmd/snap-exec/export_test.go b/cmd/snap-exec/export_test.go new file mode 100644 index 00000000..295e1fbd --- /dev/null +++ b/cmd/snap-exec/export_test.go @@ -0,0 +1,51 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +var ( + ExpandEnvCmdArgs = expandEnvCmdArgs + FindCommand = findCommand + ParseArgs = parseArgs + Run = run + ExecApp = execApp + ExecHook = execHook +) + +func MockSyscallExec(f func(argv0 string, argv []string, envv []string) (err error)) func() { + origSyscallExec := syscallExec + syscallExec = f + return func() { + syscallExec = origSyscallExec + } +} + +func SetOptsCommand(s string) { + opts.Command = s +} +func GetOptsCommand() string { + return opts.Command +} + +func SetOptsHook(s string) { + opts.Hook = s +} +func GetOptsHook() string { + return opts.Hook +} diff --git a/cmd/snap-exec/main.go b/cmd/snap-exec/main.go new file mode 100644 index 00000000..07e745cf --- /dev/null +++ b/cmd/snap-exec/main.go @@ -0,0 +1,232 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapenv" +) + +// for the tests +var syscallExec = syscall.Exec + +// commandline args +var opts struct { + Command string `long:"command" description:"use a different command like {stop,post-stop} from the app"` + Hook string `long:"hook" description:"hook to run" hidden:"yes"` +} + +func init() { + // plug/slot sanitization not used nor possible from snap-exec, make it no-op + snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {} +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "cannot snap-exec: %s\n", err) + os.Exit(1) + } +} + +func parseArgs(args []string) (app string, appArgs []string, err error) { + parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) + rest, err := parser.ParseArgs(args) + if err != nil { + return "", nil, err + } + if len(rest) == 0 { + return "", nil, fmt.Errorf("need the application to run as argument") + } + + // Catch some invalid parameter combinations, provide helpful errors + if opts.Hook != "" && opts.Command != "" { + return "", nil, fmt.Errorf("cannot use --hook and --command together") + } + if opts.Hook != "" && len(rest) > 1 { + return "", nil, fmt.Errorf("too many arguments for hook %q: %s", opts.Hook, strings.Join(rest, " ")) + } + + return rest[0], rest[1:], nil +} + +func run() error { + snapApp, extraArgs, err := parseArgs(os.Args[1:]) + if err != nil { + return err + } + + // the SNAP_REVISION is set by `snap run` - we can not (easily) + // find it in `snap-exec` because `snap-exec` is run inside the + // confinement and (generally) can not talk to snapd + revision := os.Getenv("SNAP_REVISION") + + // Now actually handle the dispatching + if opts.Hook != "" { + return execHook(snapApp, revision, opts.Hook) + } + + return execApp(snapApp, revision, opts.Command, extraArgs) +} + +const defaultShell = "/bin/bash" + +func findCommand(app *snap.AppInfo, command string) (string, error) { + var cmd string + switch command { + case "shell": + cmd = defaultShell + case "complete": + if app.Completer != "" { + cmd = defaultShell + } + case "stop": + cmd = app.StopCommand + case "reload": + cmd = app.ReloadCommand + case "post-stop": + cmd = app.PostStopCommand + case "": + cmd = app.Command + default: + return "", fmt.Errorf("cannot use %q command", command) + } + + if cmd == "" { + return "", fmt.Errorf("no %q command found for %q", command, app.Name) + } + return cmd, nil +} + +// expandEnvCmdArgs takes the string list of commandline arguments +// and expands any $VAR with the given var from the env argument. +func expandEnvCmdArgs(args []string, env map[string]string) []string { + cmdArgs := make([]string, 0, len(args)) + for _, arg := range args { + maybeExpanded := os.Expand(arg, func(k string) string { + return env[k] + }) + if maybeExpanded != "" { + cmdArgs = append(cmdArgs, maybeExpanded) + } + } + return cmdArgs +} + +func execApp(snapApp, revision, command string, args []string) error { + rev, err := snap.ParseRevision(revision) + if err != nil { + return fmt.Errorf("cannot parse revision %q: %s", revision, err) + } + + snapName, appName := snap.SplitSnapApp(snapApp) + info, err := snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: rev, + }) + if err != nil { + return fmt.Errorf("cannot read info for %q: %s", snapName, err) + } + + app := info.Apps[appName] + if app == nil { + return fmt.Errorf("cannot find app %q in %q", appName, snapName) + } + + cmdAndArgs, err := findCommand(app, command) + if err != nil { + return err + } + + // build the environment from the yaml, translating TMPDIR and + // similar variables back from where they were hidden when + // invoking the setuid snap-confine. + env := []string{} + for _, kv := range os.Environ() { + if strings.HasPrefix(kv, snapenv.PreservedUnsafePrefix) { + kv = kv[len(snapenv.PreservedUnsafePrefix):] + } + env = append(env, kv) + } + env = append(env, osutil.SubstituteEnv(app.Env())...) + + // strings.Split() is ok here because we validate all app fields + // and the whitelist is pretty strict (see + // snap/validate.go:appContentWhitelist) + tmpArgv := strings.Split(cmdAndArgs, " ") + cmd := tmpArgv[0] + cmdArgs := expandEnvCmdArgs(tmpArgv[1:], osutil.EnvMap(env)) + + // run the command + fullCmd := filepath.Join(app.Snap.MountDir(), cmd) + switch command { + case "shell": + fullCmd = defaultShell + cmdArgs = nil + case "complete": + fullCmd = defaultShell + cmdArgs = []string{ + dirs.CompletionHelper, + filepath.Join(app.Snap.MountDir(), app.Completer), + } + } + fullCmdArgs := []string{fullCmd} + fullCmdArgs = append(fullCmdArgs, cmdArgs...) + fullCmdArgs = append(fullCmdArgs, args...) + if err := syscallExec(fullCmd, fullCmdArgs, env); err != nil { + return fmt.Errorf("cannot exec %q: %s", fullCmd, err) + } + // this is never reached except in tests + return nil +} + +func execHook(snapName, revision, hookName string) error { + rev, err := snap.ParseRevision(revision) + if err != nil { + return err + } + + info, err := snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: rev, + }) + if err != nil { + return err + } + + hook := info.Hooks[hookName] + if hook == nil { + return fmt.Errorf("cannot find hook %q in %q", hookName, snapName) + } + + // build the environment + env := append(os.Environ(), osutil.SubstituteEnv(hook.Env())...) + + // run the hook + hookPath := filepath.Join(hook.Snap.HooksDir(), hook.Name) + return syscallExec(hookPath, []string{hookPath}, env) +} diff --git a/cmd/snap-exec/main_test.go b/cmd/snap-exec/main_test.go new file mode 100644 index 00000000..ad75ddf5 --- /dev/null +++ b/cmd/snap-exec/main_test.go @@ -0,0 +1,383 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + + snapExec "github.com/snapcore/snapd/cmd/snap-exec" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type snapExecSuite struct{} + +var _ = Suite(&snapExecSuite{}) + +func (s *snapExecSuite) SetUpTest(c *C) { + // clean previous parse runs + snapExec.SetOptsCommand("") + snapExec.SetOptsHook("") +} + +func (s *snapExecSuite) TearDown(c *C) { + dirs.SetRootDir("/") +} + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app cmd-arg1 $SNAP_DATA + stop-command: stop-app + post-stop-command: post-stop-app + environment: + BASE_PATH: /some/path + LD_LIBRARY_PATH: ${BASE_PATH}/lib + MY_PATH: $PATH + nostop: + command: nostop +`) + +var mockHookYaml = []byte(`name: snapname +version: 1.0 +hooks: + configure: +`) + +var mockContents = "" + +var binaryTemplate = `#!/bin/sh +echo "$(basename $0)" >> %[1]q +for arg in "$@"; do +echo "$arg" >> %[1]q +done +printf "\n" >> %[1]q` + +func (s *snapExecSuite) TestInvalidCombinedParameters(c *C) { + invalidParameters := []string{"--hook=hook-name", "--command=command-name", "snap-name"} + _, _, err := snapExec.ParseArgs(invalidParameters) + c.Check(err, ErrorMatches, ".*cannot use --hook and --command together.*") +} + +func (s *snapExecSuite) TestInvalidExtraParameters(c *C) { + invalidParameters := []string{"--hook=hook-name", "snap-name", "foo", "bar"} + _, _, err := snapExec.ParseArgs(invalidParameters) + c.Check(err, ErrorMatches, ".*too many arguments for hook \"hook-name\": snap-name foo bar.*") +} + +func (s *snapExecSuite) TestFindCommand(c *C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, IsNil) + + for _, t := range []struct { + cmd string + expected string + }{ + {cmd: "", expected: `run-app cmd-arg1 $SNAP_DATA`}, + {cmd: "stop", expected: "stop-app"}, + {cmd: "post-stop", expected: "post-stop-app"}, + } { + cmd, err := snapExec.FindCommand(info.Apps["app"], t.cmd) + c.Check(err, IsNil) + c.Check(cmd, Equals, t.expected) + } +} + +func (s *snapExecSuite) TestFindCommandInvalidCommand(c *C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, IsNil) + + _, err = snapExec.FindCommand(info.Apps["app"], "xxx") + c.Check(err, ErrorMatches, `cannot use "xxx" command`) +} + +func (s *snapExecSuite) TestFindCommandNoCommand(c *C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, IsNil) + + _, err = snapExec.FindCommand(info.Apps["nostop"], "stop") + c.Check(err, ErrorMatches, `no "stop" command found for "nostop"`) +} + +func (s *snapExecSuite) TestSnapExecAppIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + execEnv := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + }) + defer restore() + + // launch and verify its run the right way + err := snapExec.ExecApp("snapname.app", "42", "stop", []string{"arg1", "arg2"}) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/stop-app", dirs.SnapMountDir)) + c.Check(execArgs, DeepEquals, []string{execArgv0, "arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "BASE_PATH=/some/path") + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path/lib") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("MY_PATH=%s", os.Getenv("PATH"))) +} + +func (s *snapExecSuite) TestSnapExecHookIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + }) + defer restore() + + // launch and verify it ran correctly + err := snapExec.ExecHook("snapname", "42", "configure") + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/meta/hooks/configure", dirs.SnapMountDir)) + c.Check(execArgs, DeepEquals, []string{execArgv0}) +} + +func (s *snapExecSuite) TestSnapExecHookMissingHookIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + err := snapExec.ExecHook("snapname", "42", "missing-hook") + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "cannot find hook \"missing-hook\" in \"snapname\"") +} + +func (s *snapExecSuite) TestSnapExecIgnoresUnknownArgs(c *C) { + snapApp, rest, err := snapExec.ParseArgs([]string{"--command=shell", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, IsNil) + c.Assert(snapExec.GetOptsCommand(), Equals, "shell") + c.Assert(snapApp, DeepEquals, "snapname.app") + c.Assert(rest, DeepEquals, []string{"--arg1", "arg2"}) +} + +func (s *snapExecSuite) TestSnapExecErrorsOnUnknown(c *C) { + _, _, err := snapExec.ParseArgs([]string{"--command=shell", "--unknown", "snapname.app", "--arg1", "arg2"}) + c.Check(err, ErrorMatches, "unknown flag `unknown'") +} + +func (s *snapExecSuite) TestSnapExecErrorsOnMissingSnapApp(c *C) { + _, _, err := snapExec.ParseArgs([]string{"--command=shell"}) + c.Check(err, ErrorMatches, "need the application to run as argument") +} + +func (s *snapExecSuite) TestSnapExecAppRealIntegration(c *C) { + // we need a lot of mocks + dirs.SetRootDir(c.MkDir()) + + oldOsArgs := os.Args + defer func() { os.Args = oldOsArgs }() + + os.Setenv("SNAP_REVISION", "42") + defer os.Unsetenv("SNAP_REVISION") + + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + canaryFile := filepath.Join(c.MkDir(), "canary.txt") + script := fmt.Sprintf("%s/snapname/42/run-app", dirs.SnapMountDir) + err := ioutil.WriteFile(script, []byte(fmt.Sprintf(binaryTemplate, canaryFile)), 0755) + c.Assert(err, IsNil) + + // we can not use the real syscall.execv here because it would + // replace the entire test :) + restore := snapExec.MockSyscallExec(actuallyExec) + defer restore() + + // run it + os.Args = []string{"snap-exec", "snapname.app", "foo", "--bar=baz", "foobar"} + err = snapExec.Run() + c.Assert(err, IsNil) + + output, err := ioutil.ReadFile(canaryFile) + c.Assert(err, IsNil) + c.Assert(string(output), Equals, `run-app +cmd-arg1 +foo +--bar=baz +foobar + +`) +} + +func (s *snapExecSuite) TestSnapExecHookRealIntegration(c *C) { + // we need a lot of mocks + dirs.SetRootDir(c.MkDir()) + + oldOsArgs := os.Args + defer func() { os.Args = oldOsArgs }() + + os.Setenv("SNAP_REVISION", "42") + defer os.Unsetenv("SNAP_REVISION") + + canaryFile := filepath.Join(c.MkDir(), "canary.txt") + + testSnap := snaptest.MockSnap(c, string(mockHookYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + hookPath := filepath.Join("meta", "hooks", "configure") + hookPathAndContents := []string{hookPath, fmt.Sprintf(binaryTemplate, canaryFile)} + snaptest.PopulateDir(testSnap.MountDir(), [][]string{hookPathAndContents}) + hookPath = filepath.Join(testSnap.MountDir(), hookPath) + c.Assert(os.Chmod(hookPath, 0755), IsNil) + + // we can not use the real syscall.execv here because it would + // replace the entire test :) + restore := snapExec.MockSyscallExec(actuallyExec) + defer restore() + + // run it + os.Args = []string{"snap-exec", "--hook=configure", "snapname"} + err := snapExec.Run() + c.Assert(err, IsNil) + + output, err := ioutil.ReadFile(canaryFile) + c.Assert(err, IsNil) + c.Assert(string(output), Equals, "configure\n\n") +} + +func actuallyExec(argv0 string, argv []string, env []string) error { + cmd := exec.Command(argv[0], argv[1:]...) + cmd.Env = env + output, err := cmd.CombinedOutput() + if len(output) > 0 { + return fmt.Errorf("Expected output length to be 0, it was %d", len(output)) + } + return err +} + +func (s *snapExecSuite) TestSnapExecShellIntegration(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + execEnv := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + }) + defer restore() + + // launch and verify its run the right way + err := snapExec.ExecApp("snapname.app", "42", "shell", []string{"-c", "echo foo"}) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, "/bin/bash") + c.Check(execArgs, DeepEquals, []string{execArgv0, "-c", "echo foo"}) + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path/lib") +} + +func (s *snapExecSuite) TestSnapExecAppIntegrationWithVars(c *C) { + dirs.SetRootDir(c.MkDir()) + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("42"), + }) + + execArgv0 := "" + execArgs := []string{} + execEnv := []string{} + restore := snapExec.MockSyscallExec(func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + }) + defer restore() + + // setup env + os.Setenv("SNAP_DATA", "/var/snap/snapname/42") + defer os.Unsetenv("SNAP_DATA") + + // launch and verify its run the right way + err := snapExec.ExecApp("snapname.app", "42", "", []string{"user-arg1"}) + c.Assert(err, IsNil) + c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/run-app", dirs.SnapMountDir)) + c.Check(execArgs, DeepEquals, []string{execArgv0, "cmd-arg1", "/var/snap/snapname/42", "user-arg1"}) + c.Check(execEnv, testutil.Contains, "BASE_PATH=/some/path") + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path/lib") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("MY_PATH=%s", os.Getenv("PATH"))) +} + +func (s *snapExecSuite) TestSnapExecExpandEnvCmdArgs(c *C) { + for _, t := range []struct { + args []string + env map[string]string + expected []string + }{ + { + args: []string{"foo"}, + env: nil, + expected: []string{"foo"}, + }, + { + args: []string{"$var"}, + env: map[string]string{"var": "value"}, + expected: []string{"value"}, + }, + { + args: []string{"foo", "$not_existing"}, + env: nil, + expected: []string{"foo"}, + }, + { + args: []string{"foo", "$var", "baz"}, + env: map[string]string{"var": "bar", "unrelated": "env"}, + expected: []string{"foo", "bar", "baz"}, + }, + } { + c.Check(snapExec.ExpandEnvCmdArgs(t.args, t.env), DeepEquals, t.expected) + + } +} diff --git a/cmd/snap-repair/cmd_done_retry_skip.go b/cmd/snap-repair/cmd_done_retry_skip.go new file mode 100644 index 00000000..3144e5db --- /dev/null +++ b/cmd/snap-repair/cmd_done_retry_skip.go @@ -0,0 +1,82 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "strconv" +) + +func init() { + cmd, err := parser.AddCommand("done", "Signal repair is done", "", &cmdDone{}) + if err != nil { + + panic(err) + } + cmd.Hidden = true + + cmd, err = parser.AddCommand("skip", "Signal repair should be skipped", "", &cmdSkip{}) + if err != nil { + panic(err) + } + cmd.Hidden = true + + cmd, err = parser.AddCommand("retry", "Signal repair must be retried next time", "", &cmdRetry{}) + if err != nil { + panic(err) + } + cmd.Hidden = true +} + +func writeToStatusFD(msg string) error { + statusFdStr := os.Getenv("SNAP_REPAIR_STATUS_FD") + if statusFdStr == "" { + return fmt.Errorf("cannot find SNAP_REPAIR_STATUS_FD environment") + } + fd, err := strconv.Atoi(statusFdStr) + if err != nil { + return fmt.Errorf("cannot parse SNAP_REPAIR_STATUS_FD environment: %s", err) + } + f := os.NewFile(uintptr(fd), "") + defer f.Close() + if _, err := f.Write([]byte(msg + "\n")); err != nil { + return err + } + return nil +} + +type cmdDone struct{} + +func (c *cmdDone) Execute(args []string) error { + return writeToStatusFD("done") +} + +type cmdSkip struct{} + +func (c *cmdSkip) Execute([]string) error { + return writeToStatusFD("skip") +} + +type cmdRetry struct{} + +func (c *cmdRetry) Execute([]string) error { + return writeToStatusFD("retry") +} diff --git a/cmd/snap-repair/cmd_done_retry_skip_test.go b/cmd/snap-repair/cmd_done_retry_skip_test.go new file mode 100644 index 00000000..ecdf15eb --- /dev/null +++ b/cmd/snap-repair/cmd_done_retry_skip_test.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "os" + "strconv" + "syscall" + + . "gopkg.in/check.v1" + + repair "github.com/snapcore/snapd/cmd/snap-repair" +) + +func (r *repairSuite) TestStatusNoStatusFdEnv(c *C) { + for _, s := range []string{"done", "skip", "retry"} { + err := repair.ParseArgs([]string{s}) + c.Check(err, ErrorMatches, "cannot find SNAP_REPAIR_STATUS_FD environment") + } +} + +func (r *repairSuite) TestStatusBadStatusFD(c *C) { + for _, s := range []string{"done", "skip", "retry"} { + os.Setenv("SNAP_REPAIR_STATUS_FD", "123456789") + defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") + + err := repair.ParseArgs([]string{s}) + c.Check(err, ErrorMatches, `write : bad file descriptor`) + } +} + +func (r *repairSuite) TestStatusUnparsableStatusFD(c *C) { + for _, s := range []string{"done", "skip", "retry"} { + os.Setenv("SNAP_REPAIR_STATUS_FD", "xxx") + defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") + + err := repair.ParseArgs([]string{s}) + c.Check(err, ErrorMatches, `cannot parse SNAP_REPAIR_STATUS_FD environment: strconv.*: parsing "xxx": invalid syntax`) + } +} + +func (r *repairSuite) TestStatusHappy(c *C) { + for _, s := range []string{"done", "skip", "retry"} { + rp, wp, err := os.Pipe() + c.Assert(err, IsNil) + defer rp.Close() + defer wp.Close() + + fd, e := syscall.Dup(int(wp.Fd())) + c.Assert(e, IsNil) + wp.Close() + + os.Setenv("SNAP_REPAIR_STATUS_FD", strconv.Itoa(fd)) + defer os.Unsetenv("SNAP_REPAIR_STATUS_FD") + + err = repair.ParseArgs([]string{s}) + c.Check(err, IsNil) + + status, err := ioutil.ReadAll(rp) + c.Assert(err, IsNil) + c.Check(string(status), Equals, s+"\n") + } +} diff --git a/cmd/snap-repair/cmd_list.go b/cmd/snap-repair/cmd_list.go new file mode 100644 index 00000000..fa24106c --- /dev/null +++ b/cmd/snap-repair/cmd_list.go @@ -0,0 +1,74 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "text/tabwriter" +) + +func init() { + const ( + short = "Lists repairs run on this device" + long = "" + ) + + if _, err := parser.AddCommand("list", short, long, &cmdList{}); err != nil { + panic(err) + } + +} + +type cmdList struct{} + +func (c *cmdList) Execute([]string) error { + w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) + defer w.Flush() + + // FIXME: this will not currently list the repairs that are + // skipped because of e.g. wrong architecture + + // directory structure is: + // var/lib/snapd/run/repairs/ + // canonical/ + // 1/ + // r0.retry + // r0.script + // r1.done + // r1.script + // 2/ + // r3.done + // r3.script + repairTraces, err := newRepairTraces("*", "*") + if err != nil { + return err + } + if len(repairTraces) == 0 { + fmt.Fprintf(Stderr, "no repairs yet\n") + return nil + } + + fmt.Fprintf(w, "Repair\tRev\tStatus\tSummary\n") + for _, t := range repairTraces { + fmt.Fprintf(w, "%s\t%v\t%s\t%s\n", t.Repair(), t.Revision(), t.Status(), t.Summary()) + } + + return nil +} diff --git a/cmd/snap-repair/cmd_list_test.go b/cmd/snap-repair/cmd_list_test.go new file mode 100644 index 00000000..6f4e992d --- /dev/null +++ b/cmd/snap-repair/cmd_list_test.go @@ -0,0 +1,47 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + repair "github.com/snapcore/snapd/cmd/snap-repair" +) + +func (r *repairSuite) TestListNoRepairsYet(c *C) { + err := repair.ParseArgs([]string{"list"}) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, "") + c.Check(r.Stderr(), Equals, "no repairs yet\n") +} + +func (r *repairSuite) TestListRepairsSimple(c *C) { + makeMockRepairState(c) + + err := repair.ParseArgs([]string{"list"}) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, `Repair Rev Status Summary +canonical-1 3 retry repair one +my-brand-1 1 done my-brand repair one +my-brand-2 2 skip my-brand repair two +my-brand-3 0 running my-brand repair three +`) + c.Check(r.Stderr(), Equals, "") +} diff --git a/cmd/snap-repair/cmd_run.go b/cmd/snap-repair/cmd_run.go new file mode 100644 index 00000000..b31db07a --- /dev/null +++ b/cmd/snap-repair/cmd_run.go @@ -0,0 +1,102 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +func init() { + const ( + short = "Fetch and run repair assertions as necessary for the device" + long = "" + ) + + if _, err := parser.AddCommand("run", short, long, &cmdRun{}); err != nil { + panic(err) + } + +} + +type cmdRun struct{} + +var baseURL *url.URL + +func init() { + var baseurl string + if osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + baseurl = "https://api.staging.snapcraft.io/v2/" + } else { + baseurl = "https://api.snapcraft.io/v2/" + } + + var err error + baseURL, err = url.Parse(baseurl) + if err != nil { + panic(fmt.Sprintf("cannot setup base url: %v", err)) + } +} + +func (c *cmdRun) Execute(args []string) error { + if err := os.MkdirAll(dirs.SnapRunRepairDir, 0755); err != nil { + return err + } + flock, err := osutil.NewFileLock(filepath.Join(dirs.SnapRunRepairDir, "lock")) + if err != nil { + return err + } + err = flock.TryLock() + if err == osutil.ErrAlreadyLocked { + return fmt.Errorf("cannot run, another snap-repair run already executing") + } + if err != nil { + return err + } + defer flock.Unlock() + + run := NewRunner() + run.BaseURL = baseURL + err = run.LoadState() + if err != nil { + return err + } + + for { + repair, err := run.Next("canonical") + if err == ErrRepairNotFound { + // no more repairs + break + } + if err != nil { + return err + } + + if err := repair.Run(); err != nil { + return err + } + } + return nil +} diff --git a/cmd/snap-repair/cmd_run_test.go b/cmd/snap-repair/cmd_run_test.go new file mode 100644 index 00000000..96fd3a35 --- /dev/null +++ b/cmd/snap-repair/cmd_run_test.go @@ -0,0 +1,72 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" + repair "github.com/snapcore/snapd/cmd/snap-repair" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +func (r *repairSuite) TestRun(c *C) { + r1 := sysdb.InjectTrusted(r.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{r.repairRootAcctKey}) + defer r2() + + r.freshState(c) + + const script = `#!/bin/sh +echo "happy output" +echo "done" >&$SNAP_REPAIR_STATUS_FD +exit 0 +` + seqRepairs := r.signSeqRepairs(c, []string{makeMockRepair(script)}) + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + repair.MockBaseURL(mockServer.URL) + + err := repair.ParseArgs([]string{"run"}) + c.Check(err, IsNil) + c.Check(r.Stdout(), HasLen, 0) + + c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.done")), Equals, true) +} + +func (r *repairSuite) TestRunAlreadyLocked(c *C) { + err := os.MkdirAll(dirs.SnapRunRepairDir, 0700) + c.Assert(err, IsNil) + flock, err := osutil.NewFileLock(filepath.Join(dirs.SnapRunRepairDir, "lock")) + c.Assert(err, IsNil) + err = flock.Lock() + c.Assert(err, IsNil) + defer flock.Unlock() + + err = repair.ParseArgs([]string{"run"}) + c.Check(err, ErrorMatches, `cannot run, another snap-repair run already executing`) +} diff --git a/cmd/snap-repair/cmd_show.go b/cmd/snap-repair/cmd_show.go new file mode 100644 index 00000000..c95d2a91 --- /dev/null +++ b/cmd/snap-repair/cmd_show.go @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "strings" +) + +func init() { + const ( + short = "Shows specific repairs run on this device" + long = "" + ) + + if _, err := parser.AddCommand("show", short, long, &cmdShow{}); err != nil { + panic(err) + } + +} + +type cmdShow struct { + Positional struct { + Repair []string `positional-arg-name:""` + } `positional-args:"yes"` +} + +func showRepairDetails(w io.Writer, repair string) error { + i := strings.LastIndex(repair, "-") + if i < 0 { + return fmt.Errorf("cannot parse repair %q", repair) + } + brand := repair[:i] + seq := repair[i+1:] + + repairTraces, err := newRepairTraces(brand, seq) + if err != nil { + return err + } + if len(repairTraces) == 0 { + return fmt.Errorf("cannot find repair \"%s-%s\"", brand, seq) + } + + for _, trace := range repairTraces { + fmt.Fprintf(w, "repair: %s\n", trace.Repair()) + fmt.Fprintf(w, "revision: %s\n", trace.Revision()) + fmt.Fprintf(w, "status: %s\n", trace.Status()) + fmt.Fprintf(w, "summary: %s\n", trace.Summary()) + + fmt.Fprintf(w, "script:\n") + if err := trace.WriteScriptIndented(w, 2); err != nil { + fmt.Fprintf(w, "%serror: %s\n", indentPrefix(2), err) + } + + fmt.Fprintf(w, "output:\n") + if err := trace.WriteOutputIndented(w, 2); err != nil { + fmt.Fprintf(w, "%serror: %s\n", indentPrefix(2), err) + } + } + + return nil +} + +func (c *cmdShow) Execute([]string) error { + for _, repair := range c.Positional.Repair { + if err := showRepairDetails(Stdout, repair); err != nil { + return err + } + fmt.Fprintf(Stdout, "\n") + } + + return nil +} diff --git a/cmd/snap-repair/cmd_show_test.go b/cmd/snap-repair/cmd_show_test.go new file mode 100644 index 00000000..8796e218 --- /dev/null +++ b/cmd/snap-repair/cmd_show_test.go @@ -0,0 +1,142 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + repair "github.com/snapcore/snapd/cmd/snap-repair" + "github.com/snapcore/snapd/dirs" +) + +func (r *repairSuite) TestShowRepairSingle(c *C) { + makeMockRepairState(c) + + err := repair.ParseArgs([]string{"show", "canonical-1"}) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, `repair: canonical-1 +revision: 3 +status: retry +summary: repair one +script: + #!/bin/sh + echo retry output +output: + retry output + +`) + +} + +func (r *repairSuite) TestShowRepairMultiple(c *C) { + makeMockRepairState(c) + + // repair.ParseArgs() always appends to its internal slice: + // cmdShow.Positional.Repair. To workaround this we create a + // new cmdShow here + err := repair.NewCmdShow("canonical-1", "my-brand-1", "my-brand-2").Execute(nil) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, `repair: canonical-1 +revision: 3 +status: retry +summary: repair one +script: + #!/bin/sh + echo retry output +output: + retry output + +repair: my-brand-1 +revision: 1 +status: done +summary: my-brand repair one +script: + #!/bin/sh + echo done output +output: + done output + +repair: my-brand-2 +revision: 2 +status: skip +summary: my-brand repair two +script: + #!/bin/sh + echo skip output +output: + skip output + +`) +} + +func (r *repairSuite) TestShowRepairErrorNoRepairDir(c *C) { + dirs.SetRootDir(c.MkDir()) + + err := repair.NewCmdShow("canonical-1").Execute(nil) + c.Check(err, ErrorMatches, `cannot find repair "canonical-1"`) +} + +func (r *repairSuite) TestShowRepairSingleWithoutScript(c *C) { + makeMockRepairState(c) + scriptPath := filepath.Join(dirs.SnapRepairRunDir, "canonical/1", "r3.script") + err := os.Remove(scriptPath) + c.Assert(err, IsNil) + + err = repair.NewCmdShow("canonical-1").Execute(nil) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, fmt.Sprintf(`repair: canonical-1 +revision: 3 +status: retry +summary: repair one +script: + error: open %s: no such file or directory +output: + retry output + +`, scriptPath)) + +} + +func (r *repairSuite) TestShowRepairSingleUnreadableOutput(c *C) { + makeMockRepairState(c) + scriptPath := filepath.Join(dirs.SnapRepairRunDir, "canonical/1", "r3.retry") + err := os.Chmod(scriptPath, 0000) + c.Assert(err, IsNil) + defer os.Chmod(scriptPath, 0644) + + err = repair.NewCmdShow("canonical-1").Execute(nil) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, fmt.Sprintf(`repair: canonical-1 +revision: 3 +status: retry +summary: - +script: + #!/bin/sh + echo retry output +output: + error: open %s: permission denied + +`, scriptPath)) + +} diff --git a/cmd/snap-repair/export_test.go b/cmd/snap-repair/export_test.go new file mode 100644 index 00000000..a33c3994 --- /dev/null +++ b/cmd/snap-repair/export_test.go @@ -0,0 +1,142 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "net/url" + "time" + + "gopkg.in/retry.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/httputil" +) + +var ( + Parser = parser + ParseArgs = parseArgs + Run = run +) + +func MockBaseURL(baseurl string) (restore func()) { + orig := baseURL + u, err := url.Parse(baseurl) + if err != nil { + panic(err) + } + baseURL = u + return func() { + baseURL = orig + } +} + +func MockFetchRetryStrategy(strategy retry.Strategy) (restore func()) { + originalFetchRetryStrategy := fetchRetryStrategy + fetchRetryStrategy = strategy + return func() { + fetchRetryStrategy = originalFetchRetryStrategy + } +} + +func MockPeekRetryStrategy(strategy retry.Strategy) (restore func()) { + originalPeekRetryStrategy := peekRetryStrategy + peekRetryStrategy = strategy + return func() { + peekRetryStrategy = originalPeekRetryStrategy + } +} + +func MockMaxRepairScriptSize(maxSize int) (restore func()) { + originalMaxSize := maxRepairScriptSize + maxRepairScriptSize = maxSize + return func() { + maxRepairScriptSize = originalMaxSize + } +} + +func MockTrustedRepairRootKeys(keys []*asserts.AccountKey) (restore func()) { + original := trustedRepairRootKeys + trustedRepairRootKeys = keys + return func() { + trustedRepairRootKeys = original + } +} + +func TrustedRepairRootKeys() []*asserts.AccountKey { + return trustedRepairRootKeys +} + +func (run *Runner) BrandModel() (brand, model string) { + return run.state.Device.Brand, run.state.Device.Model +} + +func (run *Runner) SetStateModified(modified bool) { + run.stateModified = modified +} + +func (run *Runner) SetBrandModel(brand, model string) { + run.state.Device.Brand = brand + run.state.Device.Model = model +} + +func (run *Runner) TimeLowerBound() time.Time { + return run.state.TimeLowerBound +} + +func (run *Runner) TLSTime() time.Time { + return httputil.BaseTransport(run.cli).TLSClientConfig.Time() +} + +func (run *Runner) Sequence(brand string) []*RepairState { + return run.state.Sequences[brand] +} + +func (run *Runner) SetSequence(brand string, sequence []*RepairState) { + if run.state.Sequences == nil { + run.state.Sequences = make(map[string][]*RepairState) + } + run.state.Sequences[brand] = sequence +} + +func MockDefaultRepairTimeout(d time.Duration) (restore func()) { + orig := defaultRepairTimeout + defaultRepairTimeout = d + return func() { + defaultRepairTimeout = orig + } +} + +func MockErrtrackerReportRepair(mock func(string, string, string, map[string]string) (string, error)) (restore func()) { + prev := errtrackerReportRepair + errtrackerReportRepair = mock + return func() { errtrackerReportRepair = prev } +} + +func MockTimeNow(f func() time.Time) (restore func()) { + origTimeNow := timeNow + timeNow = f + return func() { timeNow = origTimeNow } +} + +func NewCmdShow(args ...string) *cmdShow { + cmdShow := &cmdShow{} + cmdShow.Positional.Repair = args + return cmdShow +} diff --git a/cmd/snap-repair/main.go b/cmd/snap-repair/main.go new file mode 100644 index 00000000..8b578356 --- /dev/null +++ b/cmd/snap-repair/main.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "os" + + // TODO: consider not using go-flags at all + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/release" +) + +var ( + Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + + opts struct{} + parser *flags.Parser = flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) +) + +const ( + shortHelp = "Repair an Ubuntu Core system" + longHelp = ` +snap-repair is a tool to fetch and run repair assertions +which are used to do emergency repairs on the device. +` +) + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(Stderr, "WARNING: failed to activate logging: %v\n", err) + } +} + +var errOnClassic = fmt.Errorf("cannot use snap-repair on a classic system") + +func main() { + if err := run(); err != nil { + fmt.Fprintf(Stderr, "error: %v\n", err) + if err != errOnClassic { + os.Exit(1) + } + } +} + +func run() error { + if release.OnClassic { + return errOnClassic + } + httputil.SetUserAgentFromVersion(cmd.Version, "snap-repair") + + if err := parseArgs(os.Args[1:]); err != nil { + return err + } + + return nil +} + +func parseArgs(args []string) error { + parser.ShortDescription = shortHelp + parser.LongDescription = longHelp + + _, err := parser.ParseArgs(args) + return err +} diff --git a/cmd/snap-repair/main_test.go b/cmd/snap-repair/main_test.go new file mode 100644 index 00000000..ec94bed6 --- /dev/null +++ b/cmd/snap-repair/main_test.go @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "bytes" + "testing" + + . "gopkg.in/check.v1" + + repair "github.com/snapcore/snapd/cmd/snap-repair" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type repairSuite struct { + testutil.BaseTest + baseRunnerSuite + + rootdir string + + stdout *bytes.Buffer + stderr *bytes.Buffer +} + +func (r *repairSuite) SetUpTest(c *C) { + r.BaseTest.SetUpTest(c) + r.baseRunnerSuite.SetUpTest(c) + + r.stdout = bytes.NewBuffer(nil) + r.stderr = bytes.NewBuffer(nil) + + oldStdout := repair.Stdout + r.AddCleanup(func() { repair.Stdout = oldStdout }) + repair.Stdout = r.stdout + + oldStderr := repair.Stderr + r.AddCleanup(func() { repair.Stderr = oldStderr }) + repair.Stderr = r.stderr + + r.rootdir = c.MkDir() + dirs.SetRootDir(r.rootdir) + r.AddCleanup(func() { dirs.SetRootDir("/") }) +} + +func (r *repairSuite) Stdout() string { + return r.stdout.String() +} + +func (r *repairSuite) Stderr() string { + return r.stderr.String() +} + +var _ = Suite(&repairSuite{}) + +func (r *repairSuite) TestUnknownArg(c *C) { + err := repair.ParseArgs([]string{}) + c.Check(err, ErrorMatches, "Please specify one command of: list, run or show") +} + +func (r *repairSuite) TestRunOnClassic(c *C) { + defer release.MockOnClassic(true)() + + err := repair.Run() + c.Check(err, ErrorMatches, "cannot use snap-repair on a classic system") +} diff --git a/cmd/snap-repair/runner.go b/cmd/snap-repair/runner.go new file mode 100644 index 00000000..5150c4f6 --- /dev/null +++ b/cmd/snap-repair/runner.go @@ -0,0 +1,991 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "bufio" + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "gopkg.in/retry.v1" + + "github.com/snapcore/snapd/arch" + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/errtracker" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/strutil" +) + +var ( + // TODO: move inside the repairs themselves? + defaultRepairTimeout = 30 * time.Minute +) + +var errtrackerReportRepair = errtracker.ReportRepair + +// Repair is a runnable repair. +type Repair struct { + *asserts.Repair + + run *Runner + sequence int +} + +func (r *Repair) RunDir() string { + return filepath.Join(dirs.SnapRepairRunDir, r.BrandID(), strconv.Itoa(r.RepairID())) +} + +func (r *Repair) String() string { + return fmt.Sprintf("%s-%v", r.BrandID(), r.RepairID()) +} + +// SetStatus sets the status of the repair in the state and saves the latter. +func (r *Repair) SetStatus(status RepairStatus) { + brandID := r.BrandID() + cur := *r.run.state.Sequences[brandID][r.sequence-1] + cur.Status = status + r.run.setRepairState(brandID, cur) + r.run.SaveState() +} + +// makeRepairSymlink ensures $dir/repair exists and is a symlink to +// /usr/lib/snapd/snap-repair +func makeRepairSymlink(dir string) (err error) { + // make "repair" binary available to the repair scripts via symlink + // to the real snap-repair + if err = os.MkdirAll(dir, 0755); err != nil { + return err + } + + old := filepath.Join(dirs.CoreLibExecDir, "snap-repair") + new := filepath.Join(dir, "repair") + if err := os.Symlink(old, new); err != nil && !os.IsExist(err) { + return err + } + + return nil +} + +// Run executes the repair script leaving execution trail files on disk. +func (r *Repair) Run() error { + // write the script to disk + rundir := r.RunDir() + err := os.MkdirAll(rundir, 0775) + if err != nil { + return err + } + + // ensure the script can use "repair done" + repairToolsDir := filepath.Join(dirs.SnapRunRepairDir, "tools") + if err := makeRepairSymlink(repairToolsDir); err != nil { + return err + } + + baseName := fmt.Sprintf("r%d", r.Revision()) + script := filepath.Join(rundir, baseName+".script") + err = osutil.AtomicWriteFile(script, r.Body(), 0700, 0) + if err != nil { + return err + } + + logPath := filepath.Join(rundir, baseName+".running") + logf, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer logf.Close() + + fmt.Fprintf(logf, "repair: %s\n", r) + fmt.Fprintf(logf, "revision: %d\n", r.Revision()) + fmt.Fprintf(logf, "summary: %s\n", r.Summary()) + fmt.Fprintf(logf, "output:\n") + + statusR, statusW, err := os.Pipe() + if err != nil { + return err + } + defer statusR.Close() + defer statusW.Close() + + logger.Debugf("executing %s", script) + + // run the script + env := os.Environ() + // we need to hardcode FD=3 because this is the FD after + // exec.Command() forked. there is no way in go currently + // to run something right after fork() in the child to + // know the fd. However because go will close all fds + // except the ones in "cmd.ExtraFiles" we are safe to set "3" + env = append(env, "SNAP_REPAIR_STATUS_FD=3") + env = append(env, "SNAP_REPAIR_RUN_DIR="+rundir) + // inject repairToolDir into PATH so that the script can use + // `repair {done,skip,retry}` + var havePath bool + for i, envStr := range env { + if strings.HasPrefix(envStr, "PATH=") { + newEnv := fmt.Sprintf("%s:%s", strings.TrimSuffix(envStr, ":"), repairToolsDir) + env[i] = newEnv + havePath = true + } + } + if !havePath { + env = append(env, "PATH=/usr/sbin:/usr/bin:/sbin:/bin:"+repairToolsDir) + } + + workdir := filepath.Join(rundir, "work") + if err := os.MkdirAll(workdir, 0700); err != nil { + return err + } + + cmd := exec.Command(script) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Env = env + cmd.Dir = workdir + cmd.ExtraFiles = []*os.File{statusW} + cmd.Stdout = logf + cmd.Stderr = logf + if err = cmd.Start(); err != nil { + return err + } + statusW.Close() + + // wait for repair to finish or timeout + var scriptErr error + killTimerCh := time.After(defaultRepairTimeout) + doneCh := make(chan error) + go func() { + doneCh <- cmd.Wait() + close(doneCh) + }() + select { + case scriptErr = <-doneCh: + // done + case <-killTimerCh: + if err := osutil.KillProcessGroup(cmd); err != nil { + logger.Noticef("cannot kill timed out repair %s: %s", r, err) + } + scriptErr = fmt.Errorf("repair did not finish within %s", defaultRepairTimeout) + } + // read repair status pipe, use the last value + status := readStatus(statusR) + statusPath := filepath.Join(rundir, baseName+"."+status.String()) + + // if the script had an error exit status still honor what we + // read from the status-pipe, however report the error + if scriptErr != nil { + scriptErr = fmt.Errorf("repair %s revision %d failed: %s", r, r.Revision(), scriptErr) + if err := r.errtrackerReport(scriptErr, status, logPath); err != nil { + logger.Noticef("cannot report error to errtracker: %s", err) + } + // ensure the error is present in the output log + fmt.Fprintf(logf, "\n%s", scriptErr) + } + if err := os.Rename(logPath, statusPath); err != nil { + return err + } + r.SetStatus(status) + + return nil +} + +func readStatus(r io.Reader) RepairStatus { + var status RepairStatus + scanner := bufio.NewScanner(r) + for scanner.Scan() { + switch strings.TrimSpace(scanner.Text()) { + case "done": + status = DoneStatus + // TODO: support having a script skip over many and up to a given repair-id # + case "skip": + status = SkipStatus + } + } + if scanner.Err() != nil { + return RetryStatus + } + return status +} + +// errtrackerReport reports an repairErr with the given logPath to the +// snap error tracker. +func (r *Repair) errtrackerReport(repairErr error, status RepairStatus, logPath string) error { + errMsg := repairErr.Error() + + scriptOutput, err := ioutil.ReadFile(logPath) + if err != nil { + logger.Noticef("cannot read %s", logPath) + } + s := fmt.Sprintf("%s/%d", r.BrandID(), r.RepairID()) + + dupSig := fmt.Sprintf("%s\n%s\noutput:\n%s", s, errMsg, scriptOutput) + extra := map[string]string{ + "Revision": strconv.Itoa(r.Revision()), + "BrandID": r.BrandID(), + "RepairID": strconv.Itoa(r.RepairID()), + "Status": status.String(), + } + _, err = errtrackerReportRepair(s, errMsg, dupSig, extra) + return err +} + +// Runner implements fetching, tracking and running repairs. +type Runner struct { + BaseURL *url.URL + cli *http.Client + + state state + stateModified bool + + // sequenceNext keeps track of the next integer id in a brand sequence to considered in this run, see Next. + sequenceNext map[string]int +} + +// NewRunner returns a Runner. +func NewRunner() *Runner { + run := &Runner{ + sequenceNext: make(map[string]int), + } + opts := httputil.ClientOpts{ + MayLogBody: false, + TLSConfig: &tls.Config{ + Time: run.now, + }, + } + run.cli = httputil.NewHTTPClient(&opts) + return run +} + +var ( + fetchRetryStrategy = retry.LimitCount(7, retry.LimitTime(90*time.Second, + retry.Exponential{ + Initial: 500 * time.Millisecond, + Factor: 2.5, + }, + )) + + peekRetryStrategy = retry.LimitCount(5, retry.LimitTime(44*time.Second, + retry.Exponential{ + Initial: 300 * time.Millisecond, + Factor: 2.5, + }, + )) +) + +var ( + ErrRepairNotFound = errors.New("repair not found") + ErrRepairNotModified = errors.New("repair was not modified") +) + +var ( + maxRepairScriptSize = 24 * 1024 * 1024 +) + +// Fetch retrieves a stream with the repair with the given ids and any +// auxiliary assertions. If revision>=0 the request will include an +// If-None-Match header with an ETag for the revision, and +// ErrRepairNotModified is returned if the revision is still current. +func (run *Runner) Fetch(brandID string, repairID int, revision int) (*asserts.Repair, []asserts.Assertion, error) { + u, err := run.BaseURL.Parse(fmt.Sprintf("repairs/%s/%d", brandID, repairID)) + if err != nil { + return nil, nil, err + } + + var r []asserts.Assertion + resp, err := httputil.RetryRequest(u.String(), func() (*http.Response, error) { + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/x.ubuntu.assertion") + if revision >= 0 { + req.Header.Set("If-None-Match", fmt.Sprintf(`"%d"`, revision)) + } + return run.cli.Do(req) + }, func(resp *http.Response) error { + if resp.StatusCode == 200 { + logger.Debugf("fetching repair %s-%d", brandID, repairID) + // decode assertions + dec := asserts.NewDecoderWithTypeMaxBodySize(resp.Body, map[*asserts.AssertionType]int{ + asserts.RepairType: maxRepairScriptSize, + }) + for { + a, err := dec.Decode() + if err == io.EOF { + break + } + if err != nil { + return err + } + r = append(r, a) + } + if len(r) == 0 { + return io.ErrUnexpectedEOF + } + } + return nil + }, fetchRetryStrategy) + + if err != nil { + return nil, nil, err + } + + moveTimeLowerBound := true + defer func() { + if moveTimeLowerBound { + t, _ := http.ParseTime(resp.Header.Get("Date")) + run.moveTimeLowerBound(t) + } + }() + + switch resp.StatusCode { + case 200: + // ok + case 304: + // not modified + return nil, nil, ErrRepairNotModified + case 404: + return nil, nil, ErrRepairNotFound + default: + moveTimeLowerBound = false + return nil, nil, fmt.Errorf("cannot fetch repair, unexpected status %d", resp.StatusCode) + } + + repair, aux, err := checkStream(brandID, repairID, r) + if err != nil { + return nil, nil, fmt.Errorf("cannot fetch repair, %v", err) + } + + if repair.Revision() <= revision { + // this shouldn't happen but if it does we behave like + // all the rest of assertion infrastructure and ignore + // the now superseded revision + return nil, nil, ErrRepairNotModified + } + + return repair, aux, err +} + +func checkStream(brandID string, repairID int, r []asserts.Assertion) (repair *asserts.Repair, aux []asserts.Assertion, err error) { + if len(r) == 0 { + return nil, nil, fmt.Errorf("empty repair assertions stream") + } + var ok bool + repair, ok = r[0].(*asserts.Repair) + if !ok { + return nil, nil, fmt.Errorf("unexpected first assertion %q", r[0].Type().Name) + } + + if repair.BrandID() != brandID || repair.RepairID() != repairID { + return nil, nil, fmt.Errorf("repair id mismatch %s/%d != %s/%d", repair.BrandID(), repair.RepairID(), brandID, repairID) + } + + return repair, r[1:], nil +} + +type peekResp struct { + Headers map[string]interface{} `json:"headers"` +} + +// Peek retrieves the headers for the repair with the given ids. +func (run *Runner) Peek(brandID string, repairID int) (headers map[string]interface{}, err error) { + u, err := run.BaseURL.Parse(fmt.Sprintf("repairs/%s/%d", brandID, repairID)) + if err != nil { + return nil, err + } + + var rsp peekResp + + resp, err := httputil.RetryRequest(u.String(), func() (*http.Response, error) { + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + return run.cli.Do(req) + }, func(resp *http.Response) error { + rsp.Headers = nil + if resp.StatusCode == 200 { + dec := json.NewDecoder(resp.Body) + return dec.Decode(&rsp) + } + return nil + }, peekRetryStrategy) + + if err != nil { + return nil, err + } + + moveTimeLowerBound := true + defer func() { + if moveTimeLowerBound { + t, _ := http.ParseTime(resp.Header.Get("Date")) + run.moveTimeLowerBound(t) + } + }() + + switch resp.StatusCode { + case 200: + // ok + case 404: + return nil, ErrRepairNotFound + default: + moveTimeLowerBound = false + return nil, fmt.Errorf("cannot peek repair headers, unexpected status %d", resp.StatusCode) + } + + headers = rsp.Headers + if headers["brand-id"] != brandID || headers["repair-id"] != strconv.Itoa(repairID) { + return nil, fmt.Errorf("cannot peek repair headers, repair id mismatch %s/%s != %s/%d", headers["brand-id"], headers["repair-id"], brandID, repairID) + } + + return headers, nil +} + +// deviceInfo captures information about the device. +type deviceInfo struct { + Brand string `json:"brand"` + Model string `json:"model"` +} + +// RepairStatus represents the possible statuses of a repair. +type RepairStatus int + +const ( + RetryStatus RepairStatus = iota + SkipStatus + DoneStatus +) + +func (rs RepairStatus) String() string { + switch rs { + case RetryStatus: + return "retry" + case SkipStatus: + return "skip" + case DoneStatus: + return "done" + default: + return "unknown" + } +} + +// RepairState holds the current revision and status of a repair in a sequence of repairs. +type RepairState struct { + Sequence int `json:"sequence"` + Revision int `json:"revision"` + Status RepairStatus `json:"status"` +} + +// state holds the atomically updated control state of the runner with sequences of repairs and their states. +type state struct { + Device deviceInfo `json:"device"` + Sequences map[string][]*RepairState `json:"sequences,omitempty"` + TimeLowerBound time.Time `json:"time-lower-bound"` +} + +func (run *Runner) setRepairState(brandID string, state RepairState) { + if run.state.Sequences == nil { + run.state.Sequences = make(map[string][]*RepairState) + } + sequence := run.state.Sequences[brandID] + if state.Sequence > len(sequence) { + run.stateModified = true + run.state.Sequences[brandID] = append(sequence, &state) + } else if *sequence[state.Sequence-1] != state { + run.stateModified = true + sequence[state.Sequence-1] = &state + } +} + +func (run *Runner) readState() error { + r, err := os.Open(dirs.SnapRepairStateFile) + if err != nil { + return err + } + defer r.Close() + dec := json.NewDecoder(r) + return dec.Decode(&run.state) +} + +func (run *Runner) moveTimeLowerBound(t time.Time) { + if t.After(run.state.TimeLowerBound) { + run.stateModified = true + run.state.TimeLowerBound = t.UTC() + } +} + +var timeNow = time.Now + +func (run *Runner) now() time.Time { + now := timeNow().UTC() + if now.Before(run.state.TimeLowerBound) { + return run.state.TimeLowerBound + } + return now +} + +func (run *Runner) initState() error { + if err := os.MkdirAll(dirs.SnapRepairDir, 0775); err != nil { + return fmt.Errorf("cannot create repair state directory: %v", err) + } + // best-effort remove old + os.Remove(dirs.SnapRepairStateFile) + run.state = state{} + // initialize time lower bound with image built time/seed.yaml time + info, err := os.Stat(filepath.Join(dirs.SnapSeedDir, "seed.yaml")) + if err != nil { + return err + } + run.moveTimeLowerBound(info.ModTime()) + // initialize device info + if err := run.initDeviceInfo(); err != nil { + return err + } + run.stateModified = true + return run.SaveState() +} + +func trustedBackstore(trusted []asserts.Assertion) asserts.Backstore { + trustedBS := asserts.NewMemoryBackstore() + for _, t := range trusted { + trustedBS.Put(t.Type(), t) + } + return trustedBS +} + +func checkAuthorityID(a asserts.Assertion, trusted asserts.Backstore) error { + assertType := a.Type() + if assertType != asserts.AccountKeyType && assertType != asserts.AccountType { + return nil + } + // check that account and account-key assertions are signed by + // a trusted authority + acctID := a.AuthorityID() + _, err := trusted.Get(asserts.AccountType, []string{acctID}, asserts.AccountType.MaxSupportedFormat()) + if err != nil && !asserts.IsNotFound(err) { + return err + } + if asserts.IsNotFound(err) { + return fmt.Errorf("%v not signed by trusted authority: %s", a.Ref(), acctID) + } + return nil +} + +func verifySignatures(a asserts.Assertion, workBS asserts.Backstore, trusted asserts.Backstore) error { + if err := checkAuthorityID(a, trusted); err != nil { + return err + } + acctKeyMaxSuppFormat := asserts.AccountKeyType.MaxSupportedFormat() + + seen := make(map[string]bool) + bottom := false + for !bottom { + u := a.Ref().Unique() + if seen[u] { + return fmt.Errorf("circular assertions") + } + seen[u] = true + signKey := []string{a.SignKeyID()} + key, err := trusted.Get(asserts.AccountKeyType, signKey, acctKeyMaxSuppFormat) + if err != nil && !asserts.IsNotFound(err) { + return err + } + if err == nil { + bottom = true + } else { + key, err = workBS.Get(asserts.AccountKeyType, signKey, acctKeyMaxSuppFormat) + if err != nil && !asserts.IsNotFound(err) { + return err + } + if asserts.IsNotFound(err) { + return fmt.Errorf("cannot find public key %q", signKey[0]) + } + if err := checkAuthorityID(key, trusted); err != nil { + return err + } + } + if err := asserts.CheckSignature(a, key.(*asserts.AccountKey), nil, time.Time{}); err != nil { + return err + } + a = key + } + return nil +} + +func (run *Runner) initDeviceInfo() error { + const errPrefix = "cannot set device information: " + + workBS := asserts.NewMemoryBackstore() + assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions") + dc, err := ioutil.ReadDir(assertSeedDir) + if err != nil { + return err + } + var model *asserts.Model + for _, fi := range dc { + fn := filepath.Join(assertSeedDir, fi.Name()) + f, err := os.Open(fn) + if err != nil { + // best effort + continue + } + dec := asserts.NewDecoder(f) + for { + a, err := dec.Decode() + if err != nil { + // best effort + break + } + switch a.Type() { + case asserts.ModelType: + if model != nil { + return fmt.Errorf(errPrefix + "multiple models in seed assertions") + } + model = a.(*asserts.Model) + case asserts.AccountType, asserts.AccountKeyType: + workBS.Put(a.Type(), a) + } + } + } + if model == nil { + return fmt.Errorf(errPrefix + "no model assertion in seed data") + } + trustedBS := trustedBackstore(sysdb.Trusted()) + if err := verifySignatures(model, workBS, trustedBS); err != nil { + return fmt.Errorf(errPrefix+"%v", err) + } + acctPK := []string{model.BrandID()} + acctMaxSupFormat := asserts.AccountType.MaxSupportedFormat() + acct, err := trustedBS.Get(asserts.AccountType, acctPK, acctMaxSupFormat) + if err != nil { + var err error + acct, err = workBS.Get(asserts.AccountType, acctPK, acctMaxSupFormat) + if err != nil { + return fmt.Errorf(errPrefix + "no brand account assertion in seed data") + } + } + if err := verifySignatures(acct, workBS, trustedBS); err != nil { + return fmt.Errorf(errPrefix+"%v", err) + } + run.state.Device.Brand = model.BrandID() + run.state.Device.Model = model.Model() + return nil +} + +// LoadState loads the repairs' state from disk, and (re)initializes it if it's missing or corrupted. +func (run *Runner) LoadState() error { + err := run.readState() + if err == nil { + return nil + } + // error => initialize from scratch + if !os.IsNotExist(err) { + logger.Noticef("cannor read repair state: %v", err) + } + return run.initState() +} + +// SaveState saves the repairs' state to disk. +func (run *Runner) SaveState() error { + if !run.stateModified { + return nil + } + m, err := json.Marshal(&run.state) + if err != nil { + return fmt.Errorf("cannot marshal repair state: %v", err) + } + err = osutil.AtomicWriteFile(dirs.SnapRepairStateFile, m, 0600, 0) + if err != nil { + return fmt.Errorf("cannot save repair state: %v", err) + } + run.stateModified = false + return nil +} + +func stringList(headers map[string]interface{}, name string) ([]string, error) { + v, ok := headers[name] + if !ok { + return nil, nil + } + l, ok := v.([]interface{}) + if !ok { + return nil, fmt.Errorf("header %q is not a list", name) + } + r := make([]string, len(l)) + for i, v := range l { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("header %q contains non-string elements", name) + } + r[i] = s + } + return r, nil +} + +// Applicable returns whether a repair with the given headers is applicable to the device. +func (run *Runner) Applicable(headers map[string]interface{}) bool { + if headers["disabled"] == "true" { + return false + } + series, err := stringList(headers, "series") + if err != nil { + return false + } + if len(series) != 0 && !strutil.ListContains(series, release.Series) { + return false + } + archs, err := stringList(headers, "architectures") + if err != nil { + return false + } + if len(archs) != 0 && !strutil.ListContains(archs, arch.UbuntuArchitecture()) { + return false + } + brandModel := fmt.Sprintf("%s/%s", run.state.Device.Brand, run.state.Device.Model) + models, err := stringList(headers, "models") + if err != nil { + return false + } + if len(models) != 0 && !strutil.ListContains(models, brandModel) { + // model prefix matching: brand/prefix* + hit := false + for _, patt := range models { + if strings.HasSuffix(patt, "*") && strings.ContainsRune(patt, '/') { + if strings.HasPrefix(brandModel, strings.TrimSuffix(patt, "*")) { + hit = true + break + } + } + } + if !hit { + return false + } + } + return true +} + +var errSkip = errors.New("repair unnecessary on this system") + +func (run *Runner) fetch(brandID string, repairID int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { + headers, err := run.Peek(brandID, repairID) + if err != nil { + return nil, nil, err + } + if !run.Applicable(headers) { + return nil, nil, errSkip + } + return run.Fetch(brandID, repairID, -1) +} + +func (run *Runner) refetch(brandID string, repairID, revision int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { + return run.Fetch(brandID, repairID, revision) +} + +func (run *Runner) saveStream(brandID string, repairID int, repair *asserts.Repair, aux []asserts.Assertion) error { + d := filepath.Join(dirs.SnapRepairAssertsDir, brandID, strconv.Itoa(repairID)) + err := os.MkdirAll(d, 0775) + if err != nil { + return err + } + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + r := append([]asserts.Assertion{repair}, aux...) + for _, a := range r { + if err := enc.Encode(a); err != nil { + return fmt.Errorf("cannot encode repair assertions %s-%d for saving: %v", brandID, repairID, err) + } + } + p := filepath.Join(d, fmt.Sprintf("r%d.repair", r[0].Revision())) + return osutil.AtomicWriteFile(p, buf.Bytes(), 0600, 0) +} + +func (run *Runner) readSavedStream(brandID string, repairID, revision int) (repair *asserts.Repair, aux []asserts.Assertion, err error) { + d := filepath.Join(dirs.SnapRepairAssertsDir, brandID, strconv.Itoa(repairID)) + p := filepath.Join(d, fmt.Sprintf("r%d.repair", revision)) + f, err := os.Open(p) + if err != nil { + return nil, nil, err + } + defer f.Close() + + dec := asserts.NewDecoder(f) + var r []asserts.Assertion + for { + a, err := dec.Decode() + if err == io.EOF { + break + } + if err != nil { + return nil, nil, fmt.Errorf("cannot decode repair assertions %s-%d from disk: %v", brandID, repairID, err) + } + r = append(r, a) + } + return checkStream(brandID, repairID, r) +} + +func (run *Runner) makeReady(brandID string, sequenceNext int) (repair *asserts.Repair, err error) { + sequence := run.state.Sequences[brandID] + var aux []asserts.Assertion + var state RepairState + if sequenceNext <= len(sequence) { + // consider retries + state = *sequence[sequenceNext-1] + if state.Status != RetryStatus { + return nil, errSkip + } + var err error + repair, aux, err = run.refetch(brandID, state.Sequence, state.Revision) + if err != nil { + if err != ErrRepairNotModified { + logger.Noticef("cannot refetch repair %s-%d, will retry what is on disk: %v", brandID, sequenceNext, err) + } + // try to use what we have already on disk + repair, aux, err = run.readSavedStream(brandID, state.Sequence, state.Revision) + if err != nil { + return nil, err + } + } + } else { + // fetch the next repair in the sequence + // assumes no gaps, each repair id is present so far, + // possibly skipped + var err error + repair, aux, err = run.fetch(brandID, sequenceNext) + if err != nil && err != errSkip { + return nil, err + } + state = RepairState{ + Sequence: sequenceNext, + } + if err == errSkip { + // TODO: store headers to justify decision + state.Status = SkipStatus + run.setRepairState(brandID, state) + return nil, errSkip + } + } + // verify with signatures + if err := run.Verify(repair, aux); err != nil { + return nil, fmt.Errorf("cannot verify repair %s-%d: %v", brandID, state.Sequence, err) + } + if err := run.saveStream(brandID, state.Sequence, repair, aux); err != nil { + return nil, err + } + state.Revision = repair.Revision() + if !run.Applicable(repair.Headers()) { + state.Status = SkipStatus + run.setRepairState(brandID, state) + return nil, errSkip + } + run.setRepairState(brandID, state) + return repair, nil +} + +// Next returns the next repair for the brand id sequence to run/retry or ErrRepairNotFound if there is none atm. It updates the state as required. +func (run *Runner) Next(brandID string) (*Repair, error) { + sequenceNext := run.sequenceNext[brandID] + if sequenceNext == 0 { + sequenceNext = 1 + } + for { + repair, err := run.makeReady(brandID, sequenceNext) + // SaveState is a no-op unless makeReady modified the state + stateErr := run.SaveState() + if err != nil && err != errSkip && err != ErrRepairNotFound { + // err is a non trivial error, just log the SaveState error and report err + if stateErr != nil { + logger.Noticef("%v", stateErr) + } + return nil, err + } + if stateErr != nil { + return nil, stateErr + } + if err == ErrRepairNotFound { + return nil, ErrRepairNotFound + } + + sequenceNext += 1 + run.sequenceNext[brandID] = sequenceNext + if err == errSkip { + continue + } + + return &Repair{ + Repair: repair, + run: run, + sequence: sequenceNext - 1, + }, nil + } +} + +// Limit trust to specific keys while there's no delegation or limited +// keys support. The obtained assertion stream may also include +// account keys that are directly or indirectly signed by a trusted +// key. +var ( + trustedRepairRootKeys []*asserts.AccountKey +) + +// Verify verifies that the repair is properly signed by the specific +// trusted root keys or by account keys in the stream (passed via aux) +// directly or indirectly signed by a trusted key. +func (run *Runner) Verify(repair *asserts.Repair, aux []asserts.Assertion) error { + workBS := asserts.NewMemoryBackstore() + for _, a := range aux { + if a.Type() != asserts.AccountKeyType { + continue + } + err := workBS.Put(asserts.AccountKeyType, a) + if err != nil { + return err + } + } + trustedBS := asserts.NewMemoryBackstore() + for _, t := range trustedRepairRootKeys { + trustedBS.Put(asserts.AccountKeyType, t) + } + for _, t := range sysdb.Trusted() { + if t.Type() == asserts.AccountType { + trustedBS.Put(asserts.AccountType, t) + } + } + + return verifySignatures(repair, workBS, trustedBS) +} diff --git a/cmd/snap-repair/runner_test.go b/cmd/snap-repair/runner_test.go new file mode 100644 index 00000000..ba66c342 --- /dev/null +++ b/cmd/snap-repair/runner_test.go @@ -0,0 +1,1776 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + . "gopkg.in/check.v1" + "gopkg.in/retry.v1" + + "github.com/snapcore/snapd/arch" + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" + repair "github.com/snapcore/snapd/cmd/snap-repair" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" +) + +type baseRunnerSuite struct { + tmpdir string + + seedTime time.Time + t0 time.Time + + storeSigning *assertstest.StoreStack + + brandSigning *assertstest.SigningDB + brandAcct *asserts.Account + brandAcctKey *asserts.AccountKey + + modelAs *asserts.Model + + seedAssertsDir string + + repairRootAcctKey *asserts.AccountKey + repairsAcctKey *asserts.AccountKey + + repairsSigning *assertstest.SigningDB + + restoreLogger func() +} + +func (s *baseRunnerSuite) SetUpSuite(c *C) { + s.storeSigning = assertstest.NewStoreStack("canonical", nil) + + brandPrivKey, _ := assertstest.GenerateKey(752) + + s.brandAcct = assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{ + "account-id": "my-brand", + }, "") + s.brandAcctKey = assertstest.NewAccountKey(s.storeSigning, s.brandAcct, nil, brandPrivKey.PublicKey(), "") + s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey) + + modelAs, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "brand-id": "my-brand", + "model": "my-model-2", + "architecture": "armhf", + "gadget": "gadget", + "kernel": "kernel", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + s.modelAs = modelAs.(*asserts.Model) + + repairRootKey, _ := assertstest.GenerateKey(1024) + + s.repairRootAcctKey = assertstest.NewAccountKey(s.storeSigning.RootSigning, s.storeSigning.TrustedAccount, nil, repairRootKey.PublicKey(), "") + + repairsKey, _ := assertstest.GenerateKey(752) + + repairRootSigning := assertstest.NewSigningDB("canonical", repairRootKey) + + s.repairsAcctKey = assertstest.NewAccountKey(repairRootSigning, s.storeSigning.TrustedAccount, nil, repairsKey.PublicKey(), "") + + s.repairsSigning = assertstest.NewSigningDB("canonical", repairsKey) +} + +func (s *baseRunnerSuite) SetUpTest(c *C) { + _, s.restoreLogger = logger.MockLogger() + + s.tmpdir = c.MkDir() + dirs.SetRootDir(s.tmpdir) + + s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions") + + // dummy seed yaml + err := os.MkdirAll(dirs.SnapSeedDir, 0755) + c.Assert(err, IsNil) + seedYamlFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml") + err = ioutil.WriteFile(seedYamlFn, nil, 0644) + c.Assert(err, IsNil) + seedTime, err := time.Parse(time.RFC3339, "2017-08-11T15:49:49Z") + c.Assert(err, IsNil) + err = os.Chtimes(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), seedTime, seedTime) + c.Assert(err, IsNil) + s.seedTime = seedTime + + s.t0 = time.Now().UTC().Truncate(time.Minute) +} + +func (s *baseRunnerSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") + s.restoreLogger() +} + +func (s *baseRunnerSuite) signSeqRepairs(c *C, repairs []string) []string { + var seq []string + for _, rpr := range repairs { + decoded, err := asserts.Decode([]byte(rpr)) + c.Assert(err, IsNil) + signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") + c.Assert(err, IsNil) + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + enc.Encode(signed) + enc.Encode(s.repairsAcctKey) + seq = append(seq, buf.String()) + } + return seq +} + +const freshStateJSON = `{"device":{"brand":"my-brand","model":"my-model"},"time-lower-bound":"2017-08-11T15:49:49Z"}` + +func (s *baseRunnerSuite) freshState(c *C) { + err := os.MkdirAll(dirs.SnapRepairDir, 0775) + c.Assert(err, IsNil) + err = ioutil.WriteFile(dirs.SnapRepairStateFile, []byte(freshStateJSON), 0600) + c.Assert(err, IsNil) +} + +type runnerSuite struct { + baseRunnerSuite +} + +var _ = Suite(&runnerSuite{}) + +var ( + testKey = `type: account-key +authority-id: canonical +account-id: canonical +name: repair +public-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj +since: 2015-11-16T15:04:00Z +body-length: 149 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AcZrBFaFwYABAvCX5A8dTcdLdhdiuy2YRHO5CAfM5InQefkKOhNMUq2yfi3Sk6trUHxskhZkPnm4 +NKx2yRr332q7AJXQHLX+DrZ29ycyoQ2NQGO3eAfQ0hjAAQFYBF8SSh5SutPu5XCVABEBAAE= + +AXNpZw== +` + + testRepair = `type: repair +authority-id: canonical +brand-id: canonical +repair-id: 2 +summary: repair two +architectures: + - amd64 + - arm64 +series: + - 16 +models: + - xyz/frobinator +timestamp: 2017-03-30T12:22:16Z +body-length: 7 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +script + + +AXNpZw== +` + testHeadersResp = `{"headers": +{"architectures":["amd64","arm64"],"authority-id":"canonical","body-length":"7","brand-id":"canonical","models":["xyz/frobinator"],"repair-id":"2","series":["16"],"sign-key-sha3-384":"KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj","timestamp":"2017-03-30T12:22:16Z","type":"repair"}}` +) + +func mustParseURL(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + panic(err) + } + return u +} + +func (s *runnerSuite) mockBrokenTimeNowSetToEpoch(c *C, runner *repair.Runner) (restore func()) { + epoch := time.Unix(0, 0) + r := repair.MockTimeNow(func() time.Time { + return epoch + }) + c.Check(runner.TLSTime().Equal(epoch), Equals, true) + return r +} + +func (s *runnerSuite) checkBrokenTimeNowMitigated(c *C, runner *repair.Runner) { + c.Check(runner.TLSTime().Before(s.t0), Equals, false) +} + +func (s *runnerSuite) TestFetchJustRepair(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testRepair) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + repair, aux, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, IsNil) + c.Check(repair, NotNil) + c.Check(aux, HasLen, 0) + c.Check(repair.BrandID(), Equals, "canonical") + c.Check(repair.RepairID(), Equals, 2) + c.Check(repair.Body(), DeepEquals, []byte("script\n")) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestFetchScriptTooBig(c *C) { + restore := repair.MockMaxRepairScriptSize(4) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testRepair) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, ErrorMatches, `assertion body length 7 exceeds maximum body size 4 for "repair".*`) + c.Assert(n, Equals, 1) +} + +var ( + testRetryStrategy = retry.LimitCount(5, retry.LimitTime(1*time.Second, + retry.Exponential{ + Initial: 1 * time.Millisecond, + Factor: 1, + }, + )) +) + +func (s *runnerSuite) TestFetch500(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(500) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, ErrorMatches, "cannot fetch repair, unexpected status 500") + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestFetchEmpty(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(200) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, Equals, io.ErrUnexpectedEOF) + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestFetchBroken(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(200) + io.WriteString(w, "xyz:") + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, Equals, io.ErrUnexpectedEOF) + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestFetchNotFound(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(404) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, Equals, repair.ErrRepairNotFound) + c.Assert(n, Equals, 1) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestFetchIfNoneMatchNotModified(c *C) { + restore := repair.MockFetchRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + c.Check(r.Header.Get("If-None-Match"), Equals, `"0"`) + w.WriteHeader(304) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + _, _, err := runner.Fetch("canonical", 2, 0) + c.Assert(err, Equals, repair.ErrRepairNotModified) + c.Assert(n, Equals, 1) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestFetchIgnoreSupersededRevision(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, testRepair) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, 2) + c.Assert(err, Equals, repair.ErrRepairNotModified) +} + +func (s *runnerSuite) TestFetchIdMismatch(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + io.WriteString(w, testRepair) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 4, -1) + c.Assert(err, ErrorMatches, `cannot fetch repair, repair id mismatch canonical/2 != canonical/4`) +} + +func (s *runnerSuite) TestFetchWrongFirstType(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testKey) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, _, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, ErrorMatches, `cannot fetch repair, unexpected first assertion "account-key"`) +} + +func (s *runnerSuite) TestFetchRepairPlusKey(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testRepair) + io.WriteString(w, "\n") + io.WriteString(w, testKey) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + repair, aux, err := runner.Fetch("canonical", 2, -1) + c.Assert(err, IsNil) + c.Check(repair, NotNil) + c.Check(aux, HasLen, 1) + _, ok := aux[0].(*asserts.AccountKey) + c.Check(ok, Equals, true) +} + +func (s *runnerSuite) TestPeek(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/json") + c.Check(r.URL.Path, Equals, "/repairs/canonical/2") + io.WriteString(w, testHeadersResp) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + h, err := runner.Peek("canonical", 2) + c.Assert(err, IsNil) + c.Check(h["series"], DeepEquals, []interface{}{"16"}) + c.Check(h["architectures"], DeepEquals, []interface{}{"amd64", "arm64"}) + c.Check(h["models"], DeepEquals, []interface{}{"xyz/frobinator"}) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestPeek500(c *C) { + restore := repair.MockPeekRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(500) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, err := runner.Peek("canonical", 2) + c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestPeekInvalid(c *C) { + restore := repair.MockPeekRetryStrategy(testRetryStrategy) + defer restore() + + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(200) + io.WriteString(w, "{") + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, err := runner.Peek("canonical", 2) + c.Assert(err, Equals, io.ErrUnexpectedEOF) + c.Assert(n, Equals, 5) +} + +func (s *runnerSuite) TestPeekNotFound(c *C) { + n := 0 + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n++ + w.WriteHeader(404) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + r := s.mockBrokenTimeNowSetToEpoch(c, runner) + defer r() + + _, err := runner.Peek("canonical", 2) + c.Assert(err, Equals, repair.ErrRepairNotFound) + c.Assert(n, Equals, 1) + + s.checkBrokenTimeNowMitigated(c, runner) +} + +func (s *runnerSuite) TestPeekIdMismatch(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Header.Get("Accept"), Equals, "application/json") + io.WriteString(w, testHeadersResp) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + + _, err := runner.Peek("canonical", 4) + c.Assert(err, ErrorMatches, `cannot peek repair headers, repair id mismatch canonical/2 != canonical/4`) +} + +func (s *runnerSuite) TestLoadState(c *C) { + s.freshState(c) + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + brand, model := runner.BrandModel() + c.Check(brand, Equals, "my-brand") + c.Check(model, Equals, "my-model") +} + +func (s *runnerSuite) initSeed(c *C) { + err := os.MkdirAll(s.seedAssertsDir, 0775) + c.Assert(err, IsNil) +} + +func (s *runnerSuite) writeSeedAssert(c *C, fname string, a asserts.Assertion) { + err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, fname), asserts.Encode(a), 0644) + c.Assert(err, IsNil) +} + +func (s *runnerSuite) rmSeedAssert(c *C, fname string) { + err := os.Remove(filepath.Join(s.seedAssertsDir, fname)) + c.Assert(err, IsNil) +} + +func (s *runnerSuite) TestLoadStateInitState(c *C) { + // sanity + c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) + c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) + // setup realistic seed/assertions + r := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r() + s.initSeed(c) + s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) + s.writeSeedAssert(c, "brand.account", s.brandAcct) + s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) + s.writeSeedAssert(c, "model", s.modelAs) + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, true) + + brand, model := runner.BrandModel() + c.Check(brand, Equals, "my-brand") + c.Check(model, Equals, "my-model-2") + + c.Check(runner.TimeLowerBound().Equal(s.seedTime), Equals, true) +} + +func (s *runnerSuite) TestLoadStateInitDeviceInfoFail(c *C) { + // sanity + c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) + c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) + // setup realistic seed/assertions + r := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r() + s.initSeed(c) + + const errPrefix = "cannot set device information: " + tests := []struct { + breakFunc func() + expectedErr string + }{ + {func() { s.rmSeedAssert(c, "model") }, errPrefix + "no model assertion in seed data"}, + {func() { s.rmSeedAssert(c, "brand.account") }, errPrefix + "no brand account assertion in seed data"}, + {func() { s.rmSeedAssert(c, "brand.account-key") }, errPrefix + `cannot find public key.*`}, + {func() { + // broken signature + blob := asserts.Encode(s.brandAcct) + err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, "brand.account"), blob[:len(blob)-3], 0644) + c.Assert(err, IsNil) + }, errPrefix + "cannot decode signature:.*"}, + {func() { s.writeSeedAssert(c, "model2", s.modelAs) }, errPrefix + "multiple models in seed assertions"}, + } + + for _, test := range tests { + s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) + s.writeSeedAssert(c, "brand.account", s.brandAcct) + s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) + s.writeSeedAssert(c, "model", s.modelAs) + + test.breakFunc() + + runner := repair.NewRunner() + err := runner.LoadState() + c.Check(err, ErrorMatches, test.expectedErr) + } +} + +func (s *runnerSuite) TestTLSTime(c *C) { + s.freshState(c) + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + epoch := time.Unix(0, 0) + r := repair.MockTimeNow(func() time.Time { + return epoch + }) + defer r() + c.Check(runner.TLSTime().Equal(s.seedTime), Equals, true) +} + +func makeReadOnly(c *C, dir string) (restore func()) { + // skip tests that need this because uid==0 does not honor + // write permissions in directories (yay, unix) + if os.Getuid() == 0 { + // FIXME: we could use osutil.Chattr() here + c.Skip("too lazy to make path readonly as root") + } + err := os.Chmod(dir, 0555) + c.Assert(err, IsNil) + return func() { + err := os.Chmod(dir, 0755) + c.Assert(err, IsNil) + } +} + +func (s *runnerSuite) TestLoadStateInitStateFail(c *C) { + restore := makeReadOnly(c, filepath.Dir(dirs.SnapSeedDir)) + defer restore() + + runner := repair.NewRunner() + err := runner.LoadState() + c.Check(err, ErrorMatches, `cannot create repair state directory:.*`) +} + +func (s *runnerSuite) TestSaveStateFail(c *C) { + s.freshState(c) + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + + restore := makeReadOnly(c, dirs.SnapRepairDir) + defer restore() + + // no error because this is a no-op + err = runner.SaveState() + c.Check(err, IsNil) + + // mark as modified + runner.SetStateModified(true) + + err = runner.SaveState() + c.Check(err, ErrorMatches, `cannot save repair state:.*`) +} + +func (s *runnerSuite) TestSaveState(c *C) { + s.freshState(c) + + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + + runner.SetSequence("canonical", []*repair.RepairState{ + {Sequence: 1, Revision: 3}, + }) + // mark as modified + runner.SetStateModified(true) + + err = runner.SaveState() + c.Assert(err, IsNil) + + data, err := ioutil.ReadFile(dirs.SnapRepairStateFile) + c.Assert(err, IsNil) + c.Check(string(data), Equals, `{"device":{"brand":"my-brand","model":"my-model"},"sequences":{"canonical":[{"sequence":1,"revision":3,"status":0}]},"time-lower-bound":"2017-08-11T15:49:49Z"}`) +} + +func (s *runnerSuite) TestApplicable(c *C) { + s.freshState(c) + runner := repair.NewRunner() + err := runner.LoadState() + c.Assert(err, IsNil) + + scenarios := []struct { + headers map[string]interface{} + applicable bool + }{ + {nil, true}, + {map[string]interface{}{"series": []interface{}{"18"}}, false}, + {map[string]interface{}{"series": []interface{}{"18", "16"}}, true}, + {map[string]interface{}{"series": "18"}, false}, + {map[string]interface{}{"series": []interface{}{18}}, false}, + {map[string]interface{}{"architectures": []interface{}{arch.UbuntuArchitecture()}}, true}, + {map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false}, + {map[string]interface{}{"architectures": []interface{}{"other-arch", arch.UbuntuArchitecture()}}, true}, + {map[string]interface{}{"architectures": arch.UbuntuArchitecture()}, false}, + {map[string]interface{}{"models": []interface{}{"my-brand/my-model"}}, true}, + {map[string]interface{}{"models": []interface{}{"other-brand/other-model"}}, false}, + {map[string]interface{}{"models": []interface{}{"other-brand/other-model", "my-brand/my-model"}}, true}, + {map[string]interface{}{"models": "my-brand/my-model"}, false}, + // model prefix matches + {map[string]interface{}{"models": []interface{}{"my-brand/*"}}, true}, + {map[string]interface{}{"models": []interface{}{"my-brand/my-mod*"}}, true}, + {map[string]interface{}{"models": []interface{}{"my-brand/xxx*"}}, false}, + {map[string]interface{}{"models": []interface{}{"my-brand/my-mod*", "my-brand/xxx*"}}, true}, + {map[string]interface{}{"models": []interface{}{"my*"}}, false}, + {map[string]interface{}{"disabled": "true"}, false}, + {map[string]interface{}{"disabled": "false"}, true}, + } + + for _, scen := range scenarios { + ok := runner.Applicable(scen.headers) + c.Check(ok, Equals, scen.applicable, Commentf("%v", scen)) + } +} + +var ( + nextRepairs = []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +timestamp: 2017-07-01T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptA + + +AXNpZw==`, + `type: repair +authority-id: canonical +brand-id: canonical +repair-id: 2 +summary: repair two +series: + - 33 +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`, + `type: repair +revision: 2 +authority-id: canonical +brand-id: canonical +repair-id: 3 +summary: repair three rev2 +series: + - 16 +timestamp: 2017-07-03T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptC + + +AXNpZw== +`} + + repair3Rev4 = `type: repair +revision: 4 +authority-id: canonical +brand-id: canonical +repair-id: 3 +summary: repair three rev4 +series: + - 16 +timestamp: 2017-07-03T12:00:00Z +body-length: 9 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptC2 + + +AXNpZw== +` + + repair4 = `type: repair +authority-id: canonical +brand-id: canonical +repair-id: 4 +summary: repair four +timestamp: 2017-07-03T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptD + + +AXNpZw== +` +) + +func makeMockServer(c *C, seqRepairs *[]string, redirectFirst bool) *httptest.Server { + var mockServer *httptest.Server + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + urlPath := r.URL.Path + if redirectFirst && r.Header.Get("Accept") == asserts.MediaType { + if !strings.HasPrefix(urlPath, "/final/") { + // redirect + finalURL := mockServer.URL + "/final" + r.URL.Path + w.Header().Set("Location", finalURL) + w.WriteHeader(302) + return + } + urlPath = strings.TrimPrefix(urlPath, "/final") + } + + c.Check(strings.HasPrefix(urlPath, "/repairs/canonical/"), Equals, true) + + seq, err := strconv.Atoi(strings.TrimPrefix(urlPath, "/repairs/canonical/")) + c.Assert(err, IsNil) + + if seq > len(*seqRepairs) { + w.WriteHeader(404) + return + } + + rpr := []byte((*seqRepairs)[seq-1]) + dec := asserts.NewDecoder(bytes.NewBuffer(rpr)) + repair, err := dec.Decode() + c.Assert(err, IsNil) + + switch r.Header.Get("Accept") { + case "application/json": + b, err := json.Marshal(map[string]interface{}{ + "headers": repair.Headers(), + }) + c.Assert(err, IsNil) + w.Write(b) + case asserts.MediaType: + etag := fmt.Sprintf(`"%d"`, repair.Revision()) + if strings.Contains(r.Header.Get("If-None-Match"), etag) { + w.WriteHeader(304) + return + } + w.Write(rpr) + } + })) + + c.Assert(mockServer, NotNil) + + return mockServer +} + +func (s *runnerSuite) TestTrustedRepairRootKeys(c *C) { + acctKeys := repair.TrustedRepairRootKeys() + c.Check(acctKeys, HasLen, 1) + c.Check(acctKeys[0].AccountID(), Equals, "canonical") + c.Check(acctKeys[0].PublicKeyID(), Equals, "nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t") +} + +func (s *runnerSuite) TestVerify(c *C) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + runner := repair.NewRunner() + + a, err := s.repairsSigning.Sign(asserts.RepairType, map[string]interface{}{ + "brand-id": "canonical", + "repair-id": "2", + "summary": "repair two", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, []byte("#script"), "") + c.Assert(err, IsNil) + rpr := a.(*asserts.Repair) + + err = runner.Verify(rpr, []asserts.Assertion{s.repairsAcctKey}) + c.Check(err, IsNil) +} + +func (s *runnerSuite) signSeqRepairs(c *C, repairs []string) []string { + var seq []string + for _, rpr := range repairs { + decoded, err := asserts.Decode([]byte(rpr)) + c.Assert(err, IsNil) + signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") + c.Assert(err, IsNil) + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + enc.Encode(signed) + enc.Encode(s.repairsAcctKey) + seq = append(seq, buf.String()) + } + return seq +} + +func (s *runnerSuite) loadSequences(c *C) map[string][]*repair.RepairState { + data, err := ioutil.ReadFile(dirs.SnapRepairStateFile) + c.Assert(err, IsNil) + var x struct { + Sequences map[string][]*repair.RepairState `json:"sequences"` + } + err = json.Unmarshal(data, &x) + c.Assert(err, IsNil) + return x.Sequences +} + +func (s *runnerSuite) testNext(c *C, redirectFirst bool) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + seqRepairs := s.signSeqRepairs(c, nextRepairs) + + mockServer := makeMockServer(c, &seqRepairs, redirectFirst) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err := runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 1) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "1", "r0.repair")), Equals, true) + + rpr, err = runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 3) + strm, err := ioutil.ReadFile(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "3", "r2.repair")) + c.Assert(err, IsNil) + c.Check(string(strm), Equals, seqRepairs[2]) + + // no more + rpr, err = runner.Next("canonical") + c.Check(err, Equals, repair.ErrRepairNotFound) + + expectedSeq := []*repair.RepairState{ + {Sequence: 1}, + {Sequence: 2, Status: repair.SkipStatus}, + {Sequence: 3, Revision: 2}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs := s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) + + // start fresh run with new runner + // will refetch repair 3 + signed := s.signSeqRepairs(c, []string{repair3Rev4, repair4}) + seqRepairs[2] = signed[0] + seqRepairs = append(seqRepairs, signed[1]) + + runner = repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err = runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 1) + + rpr, err = runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 3) + // refetched new revision! + c.Check(rpr.Revision(), Equals, 4) + c.Check(rpr.Body(), DeepEquals, []byte("scriptC2\n")) + + // new repair + rpr, err = runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 4) + c.Check(rpr.Body(), DeepEquals, []byte("scriptD\n")) + + // no more + rpr, err = runner.Next("canonical") + c.Check(err, Equals, repair.ErrRepairNotFound) + + c.Check(runner.Sequence("canonical"), DeepEquals, []*repair.RepairState{ + {Sequence: 1}, + {Sequence: 2, Status: repair.SkipStatus}, + {Sequence: 3, Revision: 4}, + {Sequence: 4}, + }) +} + +func (s *runnerSuite) TestNext(c *C) { + redirectFirst := false + s.testNext(c, redirectFirst) +} + +func (s *runnerSuite) TestNextRedirect(c *C) { + redirectFirst := true + s.testNext(c, redirectFirst) +} + +func (s *runnerSuite) TestNextImmediateSkip(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 33 +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + // not applicable => not returned + _, err := runner.Next("canonical") + c.Check(err, Equals, repair.ErrRepairNotFound) + + expectedSeq := []*repair.RepairState{ + {Sequence: 1, Status: repair.SkipStatus}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs := s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) +} + +func (s *runnerSuite) TestNextRefetchSkip(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 16 +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + seqRepairs = s.signSeqRepairs(c, seqRepairs) + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err := runner.Next("canonical") + c.Assert(err, IsNil) + + expectedSeq := []*repair.RepairState{ + {Sequence: 1}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs := s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) + + // new fresh run, repair becomes now unapplicable + seqRepairs[0] = `type: repair +authority-id: canonical +revision: 1 +brand-id: canonical +repair-id: 1 +summary: repair one rev1 +series: + - 16 +disabled: true +timestamp: 2017-07-02T12:00:00Z +body-length: 7 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptX + +AXNpZw==` + + seqRepairs = s.signSeqRepairs(c, seqRepairs) + + runner = repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err = runner.Next("canonical") + c.Check(err, Equals, repair.ErrRepairNotFound) + + expectedSeq = []*repair.RepairState{ + {Sequence: 1, Revision: 1, Status: repair.SkipStatus}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs = s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) +} + +func (s *runnerSuite) TestNext500(c *C) { + restore := repair.MockPeekRetryStrategy(testRetryStrategy) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err := runner.Next("canonical") + c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") +} + +func (s *runnerSuite) TestNextNotFound(c *C) { + s.freshState(c) + + restore := repair.MockPeekRetryStrategy(testRetryStrategy) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + // sanity + data, err := ioutil.ReadFile(dirs.SnapRepairStateFile) + c.Assert(err, IsNil) + c.Check(string(data), Equals, freshStateJSON) + + _, err = runner.Next("canonical") + c.Assert(err, Equals, repair.ErrRepairNotFound) + + // we saved new time lower bound + t1 := runner.TimeLowerBound() + expected := strings.Replace(freshStateJSON, "2017-08-11T15:49:49Z", t1.Format(time.RFC3339), 1) + c.Check(expected, Not(Equals), freshStateJSON) + data, err = ioutil.ReadFile(dirs.SnapRepairStateFile) + c.Assert(err, IsNil) + c.Check(string(data), Equals, expected) +} + +func (s *runnerSuite) TestNextSaveStateError(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 33 +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + // break SaveState + restore := makeReadOnly(c, dirs.SnapRepairDir) + defer restore() + + _, err := runner.Next("canonical") + c.Check(err, ErrorMatches, `cannot save repair state:.*`) +} + +func (s *runnerSuite) TestNextVerifyNoKey(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err := runner.Next("canonical") + c.Check(err, ErrorMatches, `cannot verify repair canonical-1: cannot find public key.*`) + + c.Check(runner.Sequence("canonical"), HasLen, 0) +} + +func (s *runnerSuite) TestNextVerifySelfSigned(c *C) { + randoKey, _ := assertstest.GenerateKey(752) + + randomSigning := assertstest.NewSigningDB("canonical", randoKey) + randoKeyEncoded, err := asserts.EncodePublicKey(randoKey.PublicKey()) + c.Assert(err, IsNil) + acctKey, err := randomSigning.Sign(asserts.AccountKeyType, map[string]interface{}{ + "authority-id": "canonical", + "account-id": "canonical", + "public-key-sha3-384": randoKey.PublicKey().ID(), + "name": "repairs", + "since": time.Now().UTC().Format(time.RFC3339), + }, randoKeyEncoded, "") + c.Assert(err, IsNil) + + rpr, err := randomSigning.Sign(asserts.RepairType, map[string]interface{}{ + "brand-id": "canonical", + "repair-id": "1", + "summary": "repair one", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, []byte("scriptB\n"), "") + c.Assert(err, IsNil) + + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + enc.Encode(rpr) + enc.Encode(acctKey) + seqRepairs := []string{buf.String()} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + _, err = runner.Next("canonical") + c.Check(err, ErrorMatches, `cannot verify repair canonical-1: circular assertions`) + + c.Check(runner.Sequence("canonical"), HasLen, 0) +} + +func (s *runnerSuite) TestNextVerifyAllKeysOK(c *C) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + decoded, err := asserts.Decode([]byte(nextRepairs[0])) + c.Assert(err, IsNil) + signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") + c.Assert(err, IsNil) + + // stream with all keys (any order) works as well + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + enc.Encode(signed) + enc.Encode(s.storeSigning.TrustedKey) + enc.Encode(s.repairRootAcctKey) + enc.Encode(s.repairsAcctKey) + seqRepairs := []string{buf.String()} + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err := runner.Next("canonical") + c.Assert(err, IsNil) + c.Check(rpr.RepairID(), Equals, 1) +} + +func (s *runnerSuite) TestRepairSetStatus(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +timestamp: 2017-07-02T12:00:00Z +body-length: 8 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +scriptB + + +AXNpZw==`} + + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + seqRepairs = s.signSeqRepairs(c, seqRepairs) + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err := runner.Next("canonical") + c.Assert(err, IsNil) + + rpr.SetStatus(repair.DoneStatus) + + expectedSeq := []*repair.RepairState{ + {Sequence: 1, Status: repair.DoneStatus}, + } + c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) + // on disk + seqs := s.loadSequences(c) + c.Check(seqs["canonical"], DeepEquals, expectedSeq) +} + +func (s *runnerSuite) TestRepairBasicRun(c *C) { + seqRepairs := []string{`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 16 +timestamp: 2017-07-02T12:00:00Z +body-length: 7 +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +exit 0 + + +AXNpZw==`} + + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + seqRepairs = s.signSeqRepairs(c, seqRepairs) + + mockServer := makeMockServer(c, &seqRepairs, false) + defer mockServer.Close() + + runner := repair.NewRunner() + runner.BaseURL = mustParseURL(mockServer.URL) + runner.LoadState() + + rpr, err := runner.Next("canonical") + c.Assert(err, IsNil) + + rpr.Run() + scrpt, err := ioutil.ReadFile(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script")) + c.Assert(err, IsNil) + c.Check(string(scrpt), Equals, "exit 0\n") +} + +func makeMockRepair(script string) string { + return fmt.Sprintf(`type: repair +authority-id: canonical +brand-id: canonical +repair-id: 1 +summary: repair one +series: + - 16 +timestamp: 2017-07-02T12:00:00Z +body-length: %d +sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj + +%s + +AXNpZw==`, len(script), script) +} + +func verifyRepairStatus(c *C, status repair.RepairStatus) { + data, err := ioutil.ReadFile(dirs.SnapRepairStateFile) + c.Assert(err, IsNil) + c.Check(string(data), Matches, fmt.Sprintf(`{"device":{"brand":"","model":""},"sequences":{"canonical":\[{"sequence":1,"revision":0,"status":%d}.*`, status)) +} + +// tests related to correct execution of script +type runScriptSuite struct { + baseRunnerSuite + + seqRepairs []string + + mockServer *httptest.Server + runner *repair.Runner + + runDir string + + restoreErrTrackerReportRepair func() + errReport struct { + repair string + errMsg string + dupSig string + extra map[string]string + } +} + +var _ = Suite(&runScriptSuite{}) + +func (s *runScriptSuite) SetUpTest(c *C) { + s.baseRunnerSuite.SetUpTest(c) + + s.mockServer = makeMockServer(c, &s.seqRepairs, false) + + s.runner = repair.NewRunner() + s.runner.BaseURL = mustParseURL(s.mockServer.URL) + s.runner.LoadState() + + s.runDir = filepath.Join(dirs.SnapRepairRunDir, "canonical", "1") + + s.restoreErrTrackerReportRepair = repair.MockErrtrackerReportRepair(s.errtrackerReportRepair) +} + +func (s *runScriptSuite) TearDownTest(c *C) { + s.baseRunnerSuite.TearDownTest(c) + + s.restoreErrTrackerReportRepair() + s.mockServer.Close() +} + +func (s *runScriptSuite) errtrackerReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) { + s.errReport.repair = repair + s.errReport.errMsg = errMsg + s.errReport.dupSig = dupSig + s.errReport.extra = extra + + return "some-oops-id", nil +} + +func (s *runScriptSuite) testScriptRun(c *C, mockScript string) *repair.Repair { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) + + rpr, err := s.runner.Next("canonical") + c.Assert(err, IsNil) + + err = rpr.Run() + c.Assert(err, IsNil) + + scrpt, err := ioutil.ReadFile(filepath.Join(s.runDir, "r0.script")) + c.Assert(err, IsNil) + c.Check(string(scrpt), Equals, mockScript) + + return rpr +} + +func (s *runScriptSuite) verifyRundir(c *C, names []string) { + dirents, err := ioutil.ReadDir(s.runDir) + c.Assert(err, IsNil) + c.Assert(dirents, HasLen, len(names)) + for i := range dirents { + c.Check(dirents[i].Name(), Matches, names[i]) + } +} + +type byMtime []os.FileInfo + +func (m byMtime) Len() int { return len(m) } +func (m byMtime) Less(i, j int) bool { return m[i].ModTime().Before(m[j].ModTime()) } +func (m byMtime) Swap(i, j int) { m[i], m[j] = m[j], m[i] } + +func (s *runScriptSuite) verifyOutput(c *C, name, expectedOutput string) { + output, err := ioutil.ReadFile(filepath.Join(s.runDir, name)) + c.Assert(err, IsNil) + c.Check(string(output), Equals, expectedOutput) + // ensure correct permissions + fi, err := os.Stat(filepath.Join(s.runDir, name)) + c.Assert(err, IsNil) + c.Check(fi.Mode(), Equals, os.FileMode(0600)) +} + +func (s *runScriptSuite) TestRepairBasicRunHappy(c *C) { + script := `#!/bin/sh +echo "happy output" +echo "done" >&$SNAP_REPAIR_STATUS_FD +exit 0 +` + s.seqRepairs = []string{makeMockRepair(script)} + s.testScriptRun(c, script) + // verify + s.verifyRundir(c, []string{ + `^r0.done$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.done", `repair: canonical-1 +revision: 0 +summary: repair one +output: +happy output +`) + verifyRepairStatus(c, repair.DoneStatus) +} + +func (s *runScriptSuite) TestRepairBasicRunUnhappy(c *C) { + script := `#!/bin/sh +echo "unhappy output" +exit 1 +` + s.seqRepairs = []string{makeMockRepair(script)} + s.testScriptRun(c, script) + // verify + s.verifyRundir(c, []string{ + `^r0.retry$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.retry", `repair: canonical-1 +revision: 0 +summary: repair one +output: +unhappy output + +repair canonical-1 revision 0 failed: exit status 1`) + verifyRepairStatus(c, repair.RetryStatus) + + c.Check(s.errReport.repair, Equals, "canonical/1") + c.Check(s.errReport.errMsg, Equals, `repair canonical-1 revision 0 failed: exit status 1`) + c.Check(s.errReport.dupSig, Equals, `canonical/1 +repair canonical-1 revision 0 failed: exit status 1 +output: +repair: canonical-1 +revision: 0 +summary: repair one +output: +unhappy output +`) + c.Check(s.errReport.extra, DeepEquals, map[string]string{ + "Revision": "0", + "RepairID": "1", + "BrandID": "canonical", + "Status": "retry", + }) +} + +func (s *runScriptSuite) TestRepairBasicSkip(c *C) { + script := `#!/bin/sh +echo "other output" +echo "skip" >&$SNAP_REPAIR_STATUS_FD +exit 0 +` + s.seqRepairs = []string{makeMockRepair(script)} + s.testScriptRun(c, script) + // verify + s.verifyRundir(c, []string{ + `^r0.script$`, + `^r0.skip$`, + `^work$`, + }) + s.verifyOutput(c, "r0.skip", `repair: canonical-1 +revision: 0 +summary: repair one +output: +other output +`) + verifyRepairStatus(c, repair.SkipStatus) +} + +func (s *runScriptSuite) TestRepairBasicRunUnhappyThenHappy(c *C) { + script := `#!/bin/sh +if [ -f zzz-ran-once ]; then + echo "happy now" + echo "done" >&$SNAP_REPAIR_STATUS_FD + exit 0 +fi +echo "unhappy output" +touch zzz-ran-once +exit 1 +` + s.seqRepairs = []string{makeMockRepair(script)} + rpr := s.testScriptRun(c, script) + s.verifyRundir(c, []string{ + `^r0.retry$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.retry", `repair: canonical-1 +revision: 0 +summary: repair one +output: +unhappy output + +repair canonical-1 revision 0 failed: exit status 1`) + verifyRepairStatus(c, repair.RetryStatus) + + // run again, it will be happy this time + err := rpr.Run() + c.Assert(err, IsNil) + + s.verifyRundir(c, []string{ + `^r0.done$`, + `^r0.retry$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.done", `repair: canonical-1 +revision: 0 +summary: repair one +output: +happy now +`) + verifyRepairStatus(c, repair.DoneStatus) +} + +func (s *runScriptSuite) TestRepairHitsTimeout(c *C) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + restore := repair.MockDefaultRepairTimeout(100 * time.Millisecond) + defer restore() + + script := `#!/bin/sh +echo "output before timeout" +sleep 100 +` + s.seqRepairs = []string{makeMockRepair(script)} + s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) + + rpr, err := s.runner.Next("canonical") + c.Assert(err, IsNil) + + err = rpr.Run() + c.Assert(err, IsNil) + + s.verifyRundir(c, []string{ + `^r0.retry$`, + `^r0.script$`, + `^work$`, + }) + s.verifyOutput(c, "r0.retry", `repair: canonical-1 +revision: 0 +summary: repair one +output: +output before timeout + +repair canonical-1 revision 0 failed: repair did not finish within 100ms`) + verifyRepairStatus(c, repair.RetryStatus) +} + +func (s *runScriptSuite) TestRepairHasCorrectPath(c *C) { + r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) + defer r1() + r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) + defer r2() + + script := `#!/bin/sh +echo PATH=$PATH +ls -l ${PATH##*:}/repair +` + s.seqRepairs = []string{makeMockRepair(script)} + s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) + + rpr, err := s.runner.Next("canonical") + c.Assert(err, IsNil) + + err = rpr.Run() + c.Assert(err, IsNil) + + output, err := ioutil.ReadFile(filepath.Join(s.runDir, "r0.retry")) + c.Assert(err, IsNil) + c.Check(string(output), Matches, fmt.Sprintf(`(?ms).*^PATH=.*:.*/run/snapd/repair/tools.*`)) + c.Check(string(output), Matches, `(?ms).*/repair -> /usr/lib/snapd/snap-repair`) + + // run again and ensure no error happens + err = rpr.Run() + c.Assert(err, IsNil) + +} diff --git a/cmd/snap-repair/staging.go b/cmd/snap-repair/staging.go new file mode 100644 index 00000000..629dd364 --- /dev/null +++ b/cmd/snap-repair/staging.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build withtestkeys withstagingkeys + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" +) + +const ( + encodedStagingRepairRootAccountKey = `type: account-key +authority-id: canonical +public-key-sha3-384: 0GgXgD-RtfU0HJFBmaaiUQaNUFATl1oOlzJ44Bi4MbQAwcu8ektQLGBkKQ4JuA_O +account-id: canonical +name: repair-root +since: 2017-07-07T00:00:00.0Z +body-length: 1406 +sign-key-sha3-384: e2r8on4LgdxQSaW5T8mBD5oC2fSTktTVxOYa5w3kIP4_nNF6L7mt6fOJShdOGkKu + +AcbDTQRWhcGAASAA0D+AqdqBtgiaABSZ++NdDKcvFtpmEphDfpEAAo4MAWucmE5yVN9mHFRXvwU0 +ZkLQwPuiF5Vp5EP/kHyKgmmF9nKUBXnuZXfuH4vzH9ZuEfdxGc0On+XK4KwyPUzOXj2n1Rsxj0P5 +06wJ6QFghi6nORx3Hz4pxZH7SRANgZudwWE53+whbkJyU/psv4RXfxPnu3YGo0qPk/wGCfV8kkkH +UFmeqJJEk14EGI+Kv9WlIVAqctryqf9mSXkgnhp4lzdGpsRCmRcw7boVjdOieFCJv+gVs7i52Yxu +YyiA09XF8j85DSMl4s/TyN4bm9649beBCpTJNyb3Ep6lydGiZck8ChJRLDXGP2XZtRKXsBMNIfwP +5qnLtX1/Sm1znag0grGbd3AUqL6ySAr42x8QIxZfqzk5DvQbF3xOiu2xzxTt1eB3069MlnFw99ui +fLwlec7imbCiX3bryutCRhKgkJz4MbNsyHiW51k1l1IDbABey+gfVHpeBq/2aHK5qSy98dRBSYSj +Ki++j8zR1ODequWy+OrF4cu6IQ35eunHQ2mRsJIE0xFGAjG3vCPJwzVoNS5m5R0ffncIUYdKxt/R +W2mLo43qX0fquW5LvHyI14d3B3LYfKz05FmASJaE4A+/GQhM7kMCnmykro0MM6MU0sd2OruOZVVo +z6GQ37Hyo/TGToyCr7qD/+tSq3dKYGyezl/Y4I589eqnc1DaMHL2ssiXDsbpSRHpnNqMUq6UNg4M +NsUiDLaGsNJj1ft6A7jz+yoqJ74m3hlaQK1Rot8FXBkuJGoRKBahHbh/bfkGWChDvzY9ZpoUExY2 +rp7tNYEP/LEAI/RQd03sBnqd3V8YhggT2n6QCC4ikLKvUTE3RY0qn5aAa7KC1wVi+7SfdeVl3RBF +Jyb9GCYfRUv/bfFH/TCZ9WVN1v/GIMcjBFGJf7H3cz/deela53XSaecYHuvFRpVfzmx28UcR8UY4 +5WqHfxnVQlY6DPv+kjzMzIEJGwgSAFc0d4wlSwS/Y1T0ednFRUyjMAxEUvE8tOLibtXw4q/srIFt +OIgpd/xErcyi5Ddgt7EQoYo+rtVZ8x5EwR0+i7VAV+a3bnGSJW2LFEjt2RZUiMjohVZ4oOVuoDd2 +VQzMFv41flbyqjgHhtJSCIOKDg9uI2FHbQ5vrX9qBooS68YkBALwCq+P7nSxDxFuS0CgrzSH35FX +VneOl68U74pxRgdlPJ0HI92oilrbTH8Ft0m5SzNsy+9ZZZtIDFQW+lx/ApixyifARFnZ3C3Gdx59 +FlFNbE75+X28joGtul2mPjJ1eI1dCwiFCF3R/rwfRmw3Wpv76re+EzVR1MJVCcTgC1lUoCJpKl1J +n3PQLcR8J0iqswARAQAB + +AcLBXAQAAQoABgUCWYM7bQAKCRAHKljtl9kuLtCFD/4miBm0HyLE8GdboeUtWw+oOlH0AgabRqYi +a1TpEJeYQIjnwDuCCPYtJxL1Rc+5dSNnbY9L+34NuaSyYMJY/FMuSS5iaNomGnj7YiAOds1+1/6h +Z1bTm3ttZnphg5DxckYZLaKoYgRaOzAbiRM8l+2bDbXlq3KRxZ7o7D1V/xpPis8SWK57gQ7VppHI +fcw5jnzWokWSowaKShimjJNCXMaeGdGJBLU1wcJC/XRf3tXSZecwMfL9CN/G8b17HvIFN/Pe3oS9 +QxYMQ0p3J3PF3F19Iow0VHi78hPKtVmJb5igwzBlGYFW7zZ3R35nJ7Iv6VW58G2HDDGMdBfZp930 +FbLb3mj8Yw3S5fcMZ09vpT7PK0tjFoVJtDFBOkrjvxVMEPRa0IJNcfl/hgPdp1/IFXWpZhfvk8a8 +qgzffxN+Ro/J4Jt9QrHM4sNwiEOjVvHY4cQ9GOfns9UqocmxYPDxElBNraCFOCSudZgXiyF7zUYF +OnYqTDR4ChiZtmUqIiZr6rXgZTm1raGlqR7nsbDlkJtru7tzkgMRw8xFRolaQIKiyAwTewF7vLho +imwYTRuYRMzft1q5EeRWR4XwtlIuqsXg3FCGTNIG4HiAFKrrNV7AOvVjIUSgpOcWv2leSiRQjgpY +I9oD82ii+5rKvebnGIa0o+sWhYNFoviP/49DnDNJWA== +` +) + +func init() { + repairRootAccountKey, err := asserts.Decode([]byte(encodedStagingRepairRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted account-key: %v", err)) + } + if osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + trustedRepairRootKeys = append(trustedRepairRootKeys, repairRootAccountKey.(*asserts.AccountKey)) + } +} diff --git a/cmd/snap-repair/trace.go b/cmd/snap-repair/trace.go new file mode 100644 index 00000000..9724f2ef --- /dev/null +++ b/cmd/snap-repair/trace.go @@ -0,0 +1,176 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/snapcore/snapd/dirs" +) + +// newRepairTraces returns all repairTrace about the given "brand" and "seq" +// that can be found. brand, seq can be filepath.Glob expressions. +func newRepairTraces(brand, seq string) ([]*repairTrace, error) { + matches, err := filepath.Glob(filepath.Join(dirs.SnapRepairRunDir, brand, seq, "*")) + if err != nil { + return nil, err + } + + var repairTraces []*repairTrace + for _, match := range matches { + if trace := newRepairTraceFromPath(match); trace != nil { + repairTraces = append(repairTraces, trace) + } + } + + return repairTraces, nil +} + +// repairTrace holds information about a repair that was run. +type repairTrace struct { + path string +} + +// validRepairTraceName checks that the given name looks like a valid repair +// trace +var validRepairTraceName = regexp.MustCompile(`^r[0-9]+\.(done|skip|retry|running)$`) + +// newRepairTraceFromPath takes a repair log path like +// the path /var/lib/snapd/repair/run/my-brand/1/r2.done +// and contructs a repair log from that. +func newRepairTraceFromPath(path string) *repairTrace { + rt := &repairTrace{path: path} + if !validRepairTraceName.MatchString(filepath.Base(path)) { + return nil + } + return rt +} + +// Repair returns the repair human readable string in the form $brand-$id +func (rt *repairTrace) Repair() string { + seq := filepath.Base(filepath.Dir(rt.path)) + brand := filepath.Base(filepath.Dir(filepath.Dir(rt.path))) + + return fmt.Sprintf("%s-%s", brand, seq) +} + +// Revision returns the revision of the repair +func (rt *repairTrace) Revision() string { + rev, err := revFromFilepath(rt.path) + if err != nil { + // this can never happen because we check that path starts + // with the right prefix. However handle the case just in + // case. + return "-" + } + return rev +} + +// Summary returns the summary of the repair that was run +func (rt *repairTrace) Summary() string { + f, err := os.Open(rt.path) + if err != nil { + return "-" + } + defer f.Close() + + needle := "summary: " + scanner := bufio.NewScanner(f) + for scanner.Scan() { + s := scanner.Text() + if strings.HasPrefix(s, needle) { + return s[len(needle):] + } + } + + return "-" +} + +// Status returns the status of the given repair {done,skip,retry,running} +func (rt *repairTrace) Status() string { + return filepath.Ext(rt.path)[1:] +} + +func indentPrefix(level int) string { + return strings.Repeat(" ", level) +} + +// WriteScriptIndented outputs the script that produced this repair output +// to the given writer w with the indent level given by indent. +func (rt *repairTrace) WriteScriptIndented(w io.Writer, indent int) error { + scriptPath := rt.path[:strings.LastIndex(rt.path, ".")] + ".script" + f, err := os.Open(scriptPath) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + fmt.Fprintf(w, "%s%s\n", indentPrefix(indent), scanner.Text()) + } + if scanner.Err() != nil { + return scanner.Err() + } + return nil +} + +// WriteOutputIndented outputs the repair output to the given writer w +// with the indent level given by indent. +func (rt *repairTrace) WriteOutputIndented(w io.Writer, indent int) error { + f, err := os.Open(rt.path) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + // move forward in the log to where the actual script output starts + for scanner.Scan() { + if scanner.Text() == "output:" { + break + } + } + // write the script output to w + for scanner.Scan() { + fmt.Fprintf(w, "%s%s\n", indentPrefix(indent), scanner.Text()) + } + if scanner.Err() != nil { + return scanner.Err() + } + return nil +} + +// revFromFilepath is a helper that extracts the revision number from the +// filename of the repairTrace +func revFromFilepath(name string) (string, error) { + var rev int + if _, err := fmt.Sscanf(filepath.Base(name), "r%d.", &rev); err == nil { + return strconv.Itoa(rev), nil + } + return "", fmt.Errorf("cannot find revision in %q", name) +} diff --git a/cmd/snap-repair/trace_test.go b/cmd/snap-repair/trace_test.go new file mode 100644 index 00000000..2a55bf1e --- /dev/null +++ b/cmd/snap-repair/trace_test.go @@ -0,0 +1,66 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" +) + +func makeMockRepairState(c *C) { + // the canonical script dir content + basedir := filepath.Join(dirs.SnapRepairRunDir, "canonical/1") + err := os.MkdirAll(basedir, 0700) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r3.retry"), []byte("repair: canonical-1\nsummary: repair one\noutput:\nretry output"), 0600) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r3.script"), []byte("#!/bin/sh\necho retry output"), 0700) + c.Assert(err, IsNil) + + // my-brand + basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/1") + err = os.MkdirAll(basedir, 0700) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r1.done"), []byte("repair: my-brand-1\nsummary: my-brand repair one\noutput:\ndone output"), 0600) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r1.script"), []byte("#!/bin/sh\necho done output"), 0700) + c.Assert(err, IsNil) + + basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/2") + err = os.MkdirAll(basedir, 0700) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r2.skip"), []byte("repair: my-brand-2\nsummary: my-brand repair two\noutput:\nskip output"), 0600) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r2.script"), []byte("#!/bin/sh\necho skip output"), 0700) + c.Assert(err, IsNil) + + basedir = filepath.Join(dirs.SnapRepairRunDir, "my-brand/3") + err = os.MkdirAll(basedir, 0700) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r0.running"), []byte("repair: my-brand-3\nsummary: my-brand repair three\noutput:\nrunning output"), 0600) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(basedir, "r0.script"), []byte("#!/bin/sh\necho running output"), 0700) + c.Assert(err, IsNil) +} diff --git a/cmd/snap-repair/trusted.go b/cmd/snap-repair/trusted.go new file mode 100644 index 00000000..c7599b05 --- /dev/null +++ b/cmd/snap-repair/trusted.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil" +) + +const ( + encodedRepairRootAccountKey = `type: account-key +authority-id: canonical +public-key-sha3-384: nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t +account-id: canonical +name: repair-root +since: 2017-07-07T00:00:00.0Z +body-length: 1406 +sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk + +AcbDTQRWhcGAASAAtlCIEilQCQh9Ffjn9IxD+5FtWfdKdJHrQdtFPy/2Q1kOvC++ef/3bG1xMwao +tue9K0HCMtv3apHS1C32Y8JBMD8oykRpAd5H05+sgzZr3kCHIvgogKFsXfdd5+W5Q1+59Vy/81UH +tJCs99wBwboNh/pMCXBGI3jDRN1f7hOxcHUIW+KTaHCVZnrXXmCn6Oe6brR9qiUXgEB2I6rBT/Fe +cumdfvFN/zSsJ3Vvv9IbTfHYAZD82NrSqz4UZ3WJarIaxlykgLJaZN4bqSQYPYsc8lLlwjQeGloW ++r8dIypKOzPnUYurzWcNzcCnNCT1zhpY/IK2rFcZbN5/mP2/5PtjFlbX88aPGPbOTqYANmxfboCx +wo4D4aS7PD6gLC7XM8bgh8BACpmG3BnskL7F/9IHMl85SUHFIya2fDu7A7HqNUn7cpENGbHojj7G +J2s2965FSRuIvp69wEmknYD/kahjT1+Vy94D2rVB7mjtTruPueF2KTpo2jRXFM+ABq+T9ybjXD6f +UuSXu5xeg0Cv1sxOh4O4b45uaCXb8B74chEUW+cb3cV0NGE/QgBJUBeS68vUI8lqQFmPInci6Md4 +oiKFVbloL0ZmOGj73Xv2uAcexAK9bEiI+adVS2x9r4eFwtkST3XG0t/kw7eLgAVjtRcpmD6EuZ0Q +ulAJHEsl7Sazm8GRU4GtZWaCajVb4n5TS1lin2nqUXwqxRUA3smLqkQoYXQni9vmhez4dEP4MVvq +/0IdI50UGME5Fzc8hUhYzvbNS8g+VOeAK/qj3dzcdF9+n940AQI16Fcxi1/xFk8A4dw3AaDl4XnJ +piyStE+xi95ne5HJW8r/f/JQos8I6QR5w7pe2URbgUdVPgQLv3r/4dS/X3aP+oakrPR7JuAVdP62 +vsjF4jK8Bl69mcF434xpshnbnW/f7XHomPY4gp8y7kD2/DdEs5hvaTHIPp25DEYhqjt3gfmMuUXi +Mb5oy9KZp3ff8Squ+XNWSGQSyhX14xcQwM8QjNQnAisNg2hYwSM2n8q5IDWiwJQkFSriP5tMsa8E +DMGI3LXUZKRJll9dQBjs6VzApT4/Ee0ZvSni0d2cWm3wkqFQudRpqz3JSwQ7jal0W5e0UhNeHh/W +7nACD5hvcwF7UgUz0r8adlOy+nyfvWte65nbcRrIH7GS1xdgS0e9eW4znsplp7s/Z3gMhi8CN5TY +0nZW82TTl69Wvn13SGJye0yJSjiy4KS0iRE6BwAt7dGAMs5c62IlBsWEHLmCW1/lixWA9YXT9iji +G7DKSoofnsvqVP2wIQZxxt4xHMjUGXecyx8QX4BznwsV1vbzHOIG4a3Z9A1X1L3yh3ZbazFVeEE9 +7Dhz9hGYfd3PvwARAQAB + +AcLDXAQAAQoABgUCWbuO2gAKCRDUpVvql9g3IOPcIADZWObdYMKh2SblWXxUchnc3S4LDRtL8v+Q +HdnXO5+dJmsj5LWhdsB7lz373aaylFTwHpNDWcDdAu7ulP0vr7zJURo1jGOo7VojSEeuAAu3YhwL +2pR0p5Me0wuxl/pCX0x0nfDSeeTw11kproyN0GwJaErKEmyQyfOgVr2jN5sl1gBqQtKgG5gqZzC3 +oFH1HYGPl2kfAorxFw7MoPy4aRFaxUJfx4x6bEktgkkFT7AWGmawVwcpiiUbbpe9CPLEsn6yqJI9 +5XmQ3dJjp/6Y5D7x04LRH3Q5fajRcpdBrC0tDsT9UDbSRtIyo0KDNVHwQalLa2Sv51DV+Fy4eneM +Lgu+oCUOnBecXIWnX+k0uyDW8aLHYapx8etpW3pln/hMRd8JxYVYAqDn7G2AYeSGS/4lzCJzysW2 +2/4RhH9Ql8ea0nSWVTJr3pmXKlPSH/OOy9IADEDUuEdvyMcq3YOXA9E4L3g9vR31JH+++swcTQPz +rnGx0mE+TCQRWok/NZ1QNv4eNZlnLXdNS1DoV/kRqU04cblYYUoSO34mkjPEJ8ti+VzKh/PTA6/7 +1feFX276Zam/6b2rBLWCWYdblDM9oLAR4PfzntBZW4LzzOIb95IwiK4JoDoBr3x4+RxvxgVQLvHt +8990uQ0se9+8BtLVFtd07NbldHXRBlZkq22a8CeVYrU3YZEtuEBvlGDpkdegw/vcvgHUUK1f8dXJ +0+9oW2yQOLAguuPZ67GFSgdTnvR5dQYZZR2EbQJlPMOFA3loKeUxHSR9w7E3SFqXGqN1v6APDK0V +lpVFq+rYlprvbf4GB0oR8yQOGtlxf+Ag3Nnx+90nlmtTK4k7tQpLzuAXGGREDCtn0y/YvWvGt6kN +EV5Q/mAVe2/CtAUvfyX7V3xlzYCrJT9DBcCBMaUUekFrwvZi13WYJIn7YE2Qmam7ZsXdb991PoFv ++c6Pmeg6w3y7D+Vj4Yfi8IrjPrc6765DaaZxFyMia9GEQKHChZDkiEiAM6RfwlC5YXGzCroaZi0Y +Knf/UkUWoa/jKZgQNiqrZ9oGmbURLeXkkHzpcFitwjzWr6tNScCzNIqs/uxTxbFM8fJu1gSmauEY +TE1rn62SiuHNRKJqfLcCHucStK10knHkHTAJ3avS7rBz0Dy8UOa77bOjyei5n2rkyXztL2YjjGYh +8jEt00xcvwJGePBfH10gCgTFWdfhfcP9/muKgiOSErQlHPypnr4vqO0PU9XDp106FFWyyNPd95kC +l5IF9WMfl7YHpT0Ph7kBYwg9sKF/7oCVdbT5CoImxkE5DTkWB8xX6W/BhuMrp1rzTHFFGVd1ppb7 +EMUll4dd78OWonMlIgsMRuTSn93awb4X8xSJhRi9 +` +) + +func init() { + repairRootAccountKey, err := asserts.Decode([]byte(encodedRepairRootAccountKey)) + if err != nil { + panic(fmt.Sprintf("cannot decode trusted account-key: %v", err)) + } + if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + trustedRepairRootKeys = append(trustedRepairRootKeys, repairRootAccountKey.(*asserts.AccountKey)) + } +} diff --git a/cmd/snap-seccomp/export_test.go b/cmd/snap-seccomp/export_test.go new file mode 100644 index 00000000..c49bd4d5 --- /dev/null +++ b/cmd/snap-seccomp/export_test.go @@ -0,0 +1,41 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +var ( + Compile = compile + SeccompResolver = seccompResolver +) + +func MockArchUbuntuArchitecture(f func() string) (restore func()) { + realArchUbuntuArchitecture := archUbuntuArchitecture + archUbuntuArchitecture = f + return func() { + archUbuntuArchitecture = realArchUbuntuArchitecture + } +} + +func MockArchUbuntuKernelArchitecture(f func() string) (restore func()) { + realArchUbuntuKernelArchitecture := archUbuntuKernelArchitecture + archUbuntuKernelArchitecture = f + return func() { + archUbuntuKernelArchitecture = realArchUbuntuKernelArchitecture + } +} diff --git a/cmd/snap-seccomp/main.go b/cmd/snap-seccomp/main.go new file mode 100644 index 00000000..cc9187d7 --- /dev/null +++ b/cmd/snap-seccomp/main.go @@ -0,0 +1,723 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +//#cgo CFLAGS: -D_FILE_OFFSET_BITS=64 +//#cgo pkg-config: --static --cflags libseccomp +//#cgo LDFLAGS: -Wl,-Bstatic -lseccomp -Wl,-Bdynamic +// +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +// //The XFS interface requires a 64 bit file system interface +// //but we don't want to leak this anywhere else if not globally +// //defined. +//#ifndef _FILE_OFFSET_BITS +//#define _FILE_OFFSET_BITS 64 +//#include +//#undef _FILE_OFFSET_BITS +//#else +//#include +//#endif +//#include +//#include +//#include +//#include +// +//#ifndef AF_IB +//#define AF_IB 27 +//#define PF_IB AF_IB +//#endif // AF_IB +// +//#ifndef AF_MPLS +//#define AF_MPLS 28 +//#define PF_MPLS AF_MPLS +//#endif // AF_MPLS +// +//#ifndef PR_CAP_AMBIENT +//#define PR_CAP_AMBIENT 47 +//#define PR_CAP_AMBIENT_IS_SET 1 +//#define PR_CAP_AMBIENT_RAISE 2 +//#define PR_CAP_AMBIENT_LOWER 3 +//#define PR_CAP_AMBIENT_CLEAR_ALL 4 +//#endif // PR_CAP_AMBIENT +// +//#ifndef PR_SET_THP_DISABLE +//#define PR_SET_THP_DISABLE 41 +//#endif // PR_SET_THP_DISABLE +//#ifndef PR_GET_THP_DISABLE +//#define PR_GET_THP_DISABLE 42 +//#endif // PR_GET_THP_DISABLE +// +//#ifndef PR_MPX_ENABLE_MANAGEMENT +//#define PR_MPX_ENABLE_MANAGEMENT 43 +//#endif +// +//#ifndef PR_MPX_DISABLE_MANAGEMENT +//#define PR_MPX_DISABLE_MANAGEMENT 44 +//#endif +// +// //FIXME: ARCH_BAD is defined as ~0 in libseccomp internally, however +// // this leads to a build failure on 14.04. the important part +// // is that its an invalid id for libseccomp. +// +//#define ARCH_BAD 0x7FFFFFFF +//#ifndef SCMP_ARCH_AARCH64 +//#define SCMP_ARCH_AARCH64 ARCH_BAD +//#endif +// +//#ifndef SCMP_ARCH_PPC +//#define SCMP_ARCH_PPC ARCH_BAD +//#endif +// +//#ifndef SCMP_ARCH_PPC64LE +//#define SCMP_ARCH_PPC64LE ARCH_BAD +//#endif +// +//#ifndef SCMP_ARCH_PPC64 +//#define SCMP_ARCH_PPC64 ARCH_BAD +//#endif +// +//#ifndef SCMP_ARCH_S390X +//#define SCMP_ARCH_S390X ARCH_BAD +//#endif +// +// +//typedef struct seccomp_data kernel_seccomp_data; +// +//__u32 htot32(__u32 arch, __u32 val) +//{ +// if (arch & __AUDIT_ARCH_LE) +// return htole32(val); +// else +// return htobe32(val); +//} +// +//__u64 htot64(__u32 arch, __u64 val) +//{ +// if (arch & __AUDIT_ARCH_LE) +// return htole64(val); +// else +// return htobe64(val); +//} +// +import "C" + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + "syscall" + + // FIXME: we want github.com/seccomp/libseccomp-golang but that + // will not work with trusty because libseccomp-golang checks + // for the seccomp version and errors if it find one < 2.2.0 + "github.com/mvo5/libseccomp-golang" + + "github.com/snapcore/snapd/arch" + "github.com/snapcore/snapd/osutil" +) + +// libseccomp maximum per ARG_COUNT_MAX in src/arch.h +const ScArgsMaxlength = 6 + +var seccompResolver = map[string]uint64{ + // man 2 socket - domain and man 5 apparmor.d. AF_ and PF_ are + // synonymous in the kernel and can be used interchangeably in + // policy (ie, if use AF_UNIX, don't need a corresponding PF_UNIX + // rule). See include/linux/socket.h + "AF_UNIX": syscall.AF_UNIX, + "PF_UNIX": C.PF_UNIX, + "AF_LOCAL": syscall.AF_LOCAL, + "PF_LOCAL": C.PF_LOCAL, + "AF_INET": syscall.AF_INET, + "PF_INET": C.PF_INET, + "AF_INET6": syscall.AF_INET6, + "PF_INET6": C.PF_INET6, + "AF_IPX": syscall.AF_IPX, + "PF_IPX": C.PF_IPX, + "AF_NETLINK": syscall.AF_NETLINK, + "PF_NETLINK": C.PF_NETLINK, + "AF_X25": syscall.AF_X25, + "PF_X25": C.PF_X25, + "AF_AX25": syscall.AF_AX25, + "PF_AX25": C.PF_AX25, + "AF_ATMPVC": syscall.AF_ATMPVC, + "PF_ATMPVC": C.PF_ATMPVC, + "AF_APPLETALK": syscall.AF_APPLETALK, + "PF_APPLETALK": C.PF_APPLETALK, + "AF_PACKET": syscall.AF_PACKET, + "PF_PACKET": C.PF_PACKET, + "AF_ALG": syscall.AF_ALG, + "PF_ALG": C.PF_ALG, + "AF_BRIDGE": syscall.AF_BRIDGE, + "PF_BRIDGE": C.PF_BRIDGE, + "AF_NETROM": syscall.AF_NETROM, + "PF_NETROM": C.PF_NETROM, + "AF_ROSE": syscall.AF_ROSE, + "PF_ROSE": C.PF_ROSE, + "AF_NETBEUI": syscall.AF_NETBEUI, + "PF_NETBEUI": C.PF_NETBEUI, + "AF_SECURITY": syscall.AF_SECURITY, + "PF_SECURITY": C.PF_SECURITY, + "AF_KEY": syscall.AF_KEY, + "PF_KEY": C.PF_KEY, + "AF_ASH": syscall.AF_ASH, + "PF_ASH": C.PF_ASH, + "AF_ECONET": syscall.AF_ECONET, + "PF_ECONET": C.PF_ECONET, + "AF_SNA": syscall.AF_SNA, + "PF_SNA": C.PF_SNA, + "AF_IRDA": syscall.AF_IRDA, + "PF_IRDA": C.PF_IRDA, + "AF_PPPOX": syscall.AF_PPPOX, + "PF_PPPOX": C.PF_PPPOX, + "AF_WANPIPE": syscall.AF_WANPIPE, + "PF_WANPIPE": C.PF_WANPIPE, + "AF_BLUETOOTH": syscall.AF_BLUETOOTH, + "PF_BLUETOOTH": C.PF_BLUETOOTH, + "AF_RDS": syscall.AF_RDS, + "PF_RDS": C.PF_RDS, + "AF_LLC": syscall.AF_LLC, + "PF_LLC": C.PF_LLC, + "AF_TIPC": syscall.AF_TIPC, + "PF_TIPC": C.PF_TIPC, + "AF_IUCV": syscall.AF_IUCV, + "PF_IUCV": C.PF_IUCV, + "AF_RXRPC": syscall.AF_RXRPC, + "PF_RXRPC": C.PF_RXRPC, + "AF_ISDN": syscall.AF_ISDN, + "PF_ISDN": C.PF_ISDN, + "AF_PHONET": syscall.AF_PHONET, + "PF_PHONET": C.PF_PHONET, + "AF_IEEE802154": syscall.AF_IEEE802154, + "PF_IEEE802154": C.PF_IEEE802154, + "AF_CAIF": syscall.AF_CAIF, + "PF_CAIF": C.AF_CAIF, + "AF_NFC": C.AF_NFC, + "PF_NFC": C.PF_NFC, + "AF_VSOCK": C.AF_VSOCK, + "PF_VSOCK": C.PF_VSOCK, + // may not be defined in socket.h yet + "AF_IB": C.AF_IB, // 27 + "PF_IB": C.PF_IB, + "AF_MPLS": C.AF_MPLS, // 28 + "PF_MPLS": C.PF_MPLS, + "AF_CAN": syscall.AF_CAN, + "PF_CAN": C.PF_CAN, + + // man 2 socket - type + "SOCK_STREAM": syscall.SOCK_STREAM, + "SOCK_DGRAM": syscall.SOCK_DGRAM, + "SOCK_SEQPACKET": syscall.SOCK_SEQPACKET, + "SOCK_RAW": syscall.SOCK_RAW, + "SOCK_RDM": syscall.SOCK_RDM, + "SOCK_PACKET": syscall.SOCK_PACKET, + + // man 2 prctl + "PR_CAP_AMBIENT": C.PR_CAP_AMBIENT, + "PR_CAP_AMBIENT_RAISE": C.PR_CAP_AMBIENT_RAISE, + "PR_CAP_AMBIENT_LOWER": C.PR_CAP_AMBIENT_LOWER, + "PR_CAP_AMBIENT_IS_SET": C.PR_CAP_AMBIENT_IS_SET, + "PR_CAP_AMBIENT_CLEAR_ALL": C.PR_CAP_AMBIENT_CLEAR_ALL, + "PR_CAPBSET_READ": C.PR_CAPBSET_READ, + "PR_CAPBSET_DROP": C.PR_CAPBSET_DROP, + "PR_SET_CHILD_SUBREAPER": C.PR_SET_CHILD_SUBREAPER, + "PR_GET_CHILD_SUBREAPER": C.PR_GET_CHILD_SUBREAPER, + "PR_SET_DUMPABLE": C.PR_SET_DUMPABLE, + "PR_GET_DUMPABLE": C.PR_GET_DUMPABLE, + "PR_SET_ENDIAN": C.PR_SET_ENDIAN, + "PR_GET_ENDIAN": C.PR_GET_ENDIAN, + "PR_SET_FPEMU": C.PR_SET_FPEMU, + "PR_GET_FPEMU": C.PR_GET_FPEMU, + "PR_SET_FPEXC": C.PR_SET_FPEXC, + "PR_GET_FPEXC": C.PR_GET_FPEXC, + "PR_SET_KEEPCAPS": C.PR_SET_KEEPCAPS, + "PR_GET_KEEPCAPS": C.PR_GET_KEEPCAPS, + "PR_MCE_KILL": C.PR_MCE_KILL, + "PR_MCE_KILL_GET": C.PR_MCE_KILL_GET, + "PR_SET_MM": C.PR_SET_MM, + "PR_SET_MM_START_CODE": C.PR_SET_MM_START_CODE, + "PR_SET_MM_END_CODE": C.PR_SET_MM_END_CODE, + "PR_SET_MM_START_DATA": C.PR_SET_MM_START_DATA, + "PR_SET_MM_END_DATA": C.PR_SET_MM_END_DATA, + "PR_SET_MM_START_STACK": C.PR_SET_MM_START_STACK, + "PR_SET_MM_START_BRK": C.PR_SET_MM_START_BRK, + "PR_SET_MM_BRK": C.PR_SET_MM_BRK, + "PR_SET_MM_ARG_START": C.PR_SET_MM_ARG_START, + "PR_SET_MM_ARG_END": C.PR_SET_MM_ARG_END, + "PR_SET_MM_ENV_START": C.PR_SET_MM_ENV_START, + "PR_SET_MM_ENV_END": C.PR_SET_MM_ENV_END, + "PR_SET_MM_AUXV": C.PR_SET_MM_AUXV, + "PR_SET_MM_EXE_FILE": C.PR_SET_MM_EXE_FILE, + "PR_MPX_ENABLE_MANAGEMENT": C.PR_MPX_ENABLE_MANAGEMENT, + "PR_MPX_DISABLE_MANAGEMENT": C.PR_MPX_DISABLE_MANAGEMENT, + "PR_SET_NAME": C.PR_SET_NAME, + "PR_GET_NAME": C.PR_GET_NAME, + "PR_SET_NO_NEW_PRIVS": C.PR_SET_NO_NEW_PRIVS, + "PR_GET_NO_NEW_PRIVS": C.PR_GET_NO_NEW_PRIVS, + "PR_SET_PDEATHSIG": C.PR_SET_PDEATHSIG, + "PR_GET_PDEATHSIG": C.PR_GET_PDEATHSIG, + "PR_SET_PTRACER": C.PR_SET_PTRACER, + "PR_SET_SECCOMP": C.PR_SET_SECCOMP, + "PR_GET_SECCOMP": C.PR_GET_SECCOMP, + "PR_SET_SECUREBITS": C.PR_SET_SECUREBITS, + "PR_GET_SECUREBITS": C.PR_GET_SECUREBITS, + "PR_SET_THP_DISABLE": C.PR_SET_THP_DISABLE, + "PR_TASK_PERF_EVENTS_DISABLE": C.PR_TASK_PERF_EVENTS_DISABLE, + "PR_TASK_PERF_EVENTS_ENABLE": C.PR_TASK_PERF_EVENTS_ENABLE, + "PR_GET_THP_DISABLE": C.PR_GET_THP_DISABLE, + "PR_GET_TID_ADDRESS": C.PR_GET_TID_ADDRESS, + "PR_SET_TIMERSLACK": C.PR_SET_TIMERSLACK, + "PR_GET_TIMERSLACK": C.PR_GET_TIMERSLACK, + "PR_SET_TIMING": C.PR_SET_TIMING, + "PR_GET_TIMING": C.PR_GET_TIMING, + "PR_SET_TSC": C.PR_SET_TSC, + "PR_GET_TSC": C.PR_GET_TSC, + "PR_SET_UNALIGN": C.PR_SET_UNALIGN, + "PR_GET_UNALIGN": C.PR_GET_UNALIGN, + + // man 2 getpriority + "PRIO_PROCESS": syscall.PRIO_PROCESS, + "PRIO_PGRP": syscall.PRIO_PGRP, + "PRIO_USER": syscall.PRIO_USER, + + // man 2 setns + "CLONE_NEWIPC": syscall.CLONE_NEWIPC, + "CLONE_NEWNET": syscall.CLONE_NEWNET, + "CLONE_NEWNS": syscall.CLONE_NEWNS, + "CLONE_NEWPID": syscall.CLONE_NEWPID, + "CLONE_NEWUSER": syscall.CLONE_NEWUSER, + "CLONE_NEWUTS": syscall.CLONE_NEWUTS, + + // man 4 tty_ioctl + "TIOCSTI": syscall.TIOCSTI, + + // man 2 quotactl (with what Linux supports) + "Q_SYNC": C.Q_SYNC, + "Q_QUOTAON": C.Q_QUOTAON, + "Q_QUOTAOFF": C.Q_QUOTAOFF, + "Q_GETFMT": C.Q_GETFMT, + "Q_GETINFO": C.Q_GETINFO, + "Q_SETINFO": C.Q_SETINFO, + "Q_GETQUOTA": C.Q_GETQUOTA, + "Q_SETQUOTA": C.Q_SETQUOTA, + "Q_XQUOTAON": C.Q_XQUOTAON, + "Q_XQUOTAOFF": C.Q_XQUOTAOFF, + "Q_XGETQUOTA": C.Q_XGETQUOTA, + "Q_XSETQLIM": C.Q_XSETQLIM, + "Q_XGETQSTAT": C.Q_XGETQSTAT, + "Q_XQUOTARM": C.Q_XQUOTARM, + + // man 2 mknod + "S_IFREG": syscall.S_IFREG, + "S_IFCHR": syscall.S_IFCHR, + "S_IFBLK": syscall.S_IFBLK, + "S_IFIFO": syscall.S_IFIFO, + "S_IFSOCK": syscall.S_IFSOCK, + + // man 7 netlink (uapi/linux/netlink.h) + "NETLINK_ROUTE": syscall.NETLINK_ROUTE, + "NETLINK_USERSOCK": syscall.NETLINK_USERSOCK, + "NETLINK_FIREWALL": syscall.NETLINK_FIREWALL, + "NETLINK_SOCK_DIAG": C.NETLINK_SOCK_DIAG, + "NETLINK_NFLOG": syscall.NETLINK_NFLOG, + "NETLINK_XFRM": syscall.NETLINK_XFRM, + "NETLINK_SELINUX": syscall.NETLINK_SELINUX, + "NETLINK_ISCSI": syscall.NETLINK_ISCSI, + "NETLINK_AUDIT": syscall.NETLINK_AUDIT, + "NETLINK_FIB_LOOKUP": syscall.NETLINK_FIB_LOOKUP, + "NETLINK_CONNECTOR": syscall.NETLINK_CONNECTOR, + "NETLINK_NETFILTER": syscall.NETLINK_NETFILTER, + "NETLINK_IP6_FW": syscall.NETLINK_IP6_FW, + "NETLINK_DNRTMSG": syscall.NETLINK_DNRTMSG, + "NETLINK_KOBJECT_UEVENT": syscall.NETLINK_KOBJECT_UEVENT, + "NETLINK_GENERIC": syscall.NETLINK_GENERIC, + "NETLINK_SCSITRANSPORT": syscall.NETLINK_SCSITRANSPORT, + "NETLINK_ECRYPTFS": syscall.NETLINK_ECRYPTFS, + "NETLINK_RDMA": C.NETLINK_RDMA, + "NETLINK_CRYPTO": C.NETLINK_CRYPTO, + "NETLINK_INET_DIAG": C.NETLINK_INET_DIAG, // synonymous with NETLINK_SOCK_DIAG +} + +const ( + SeccompRetAllow = C.SECCOMP_RET_ALLOW + SeccompRetKill = C.SECCOMP_RET_KILL +) + +// UbuntuArchToScmpArch takes a dpkg architecture and converts it to +// the seccomp.ScmpArch as used in the libseccomp-golang library +func UbuntuArchToScmpArch(ubuntuArch string) seccomp.ScmpArch { + switch ubuntuArch { + case "amd64": + return seccomp.ArchAMD64 + case "arm64": + return seccomp.ArchARM64 + case "armhf": + return seccomp.ArchARM + case "i386": + return seccomp.ArchX86 + case "powerpc": + return seccomp.ArchPPC + case "ppc64": + return seccomp.ArchPPC64 + case "ppc64el": + return seccomp.ArchPPC64LE + case "s390x": + return seccomp.ArchS390X + } + panic(fmt.Sprintf("cannot map ubuntu arch %q to a seccomp arch", ubuntuArch)) +} + +// ScmpArchToSeccompNativeArch takes a seccomp.ScmpArch and converts +// it into the native kernel architecture uint32. This is required for +// the tests to simulate the bpf kernel behaviour. +func ScmpArchToSeccompNativeArch(scmpArch seccomp.ScmpArch) uint32 { + switch scmpArch { + case seccomp.ArchAMD64: + return C.SCMP_ARCH_X86_64 + case seccomp.ArchARM64: + return C.SCMP_ARCH_AARCH64 + case seccomp.ArchARM: + return C.SCMP_ARCH_ARM + case seccomp.ArchPPC64: + return C.SCMP_ARCH_PPC64 + case seccomp.ArchPPC64LE: + return C.SCMP_ARCH_PPC64LE + case seccomp.ArchPPC: + return C.SCMP_ARCH_PPC + case seccomp.ArchS390X: + return C.SCMP_ARCH_S390X + case seccomp.ArchX86: + return C.SCMP_ARCH_X86 + } + panic(fmt.Sprintf("cannot map scmpArch %q to a native seccomp arch", scmpArch)) +} + +// important for unit testing +type SeccompData C.kernel_seccomp_data + +func (sc *SeccompData) SetNr(nr seccomp.ScmpSyscall) { + sc.nr = C.int(C.htot32(C.__u32(sc.arch), C.__u32(nr))) +} +func (sc *SeccompData) SetArch(arch uint32) { + sc.arch = C.htot32(C.__u32(arch), C.__u32(arch)) +} +func (sc *SeccompData) SetArgs(args [6]uint64) { + for i := range args { + sc.args[i] = C.htot64(sc.arch, C.__u64(args[i])) + } +} + +func readNumber(token string) (uint64, error) { + if value, ok := seccompResolver[token]; ok { + return value, nil + } + + // Negative numbers are not supported yet, but when they are, + // adjust this accordingly + return strconv.ParseUint(token, 10, 64) +} + +func parseLine(line string, secFilter *seccomp.ScmpFilter) error { + // ignore comments and empty lines + if strings.HasPrefix(line, "#") || line == "" { + return nil + } + + // regular line + tokens := strings.Fields(line) + if len(tokens[1:]) > ScArgsMaxlength { + return fmt.Errorf("too many arguments specified for syscall '%s' in line %q", tokens[0], line) + } + + // fish out syscall + secSyscall, err := seccomp.GetSyscallFromName(tokens[0]) + if err != nil { + // FIXME: use structed error in libseccomp-golang when + // https://github.com/seccomp/libseccomp-golang/pull/26 + // gets merged. For now, ignore + // unknown syscalls + return nil + } + + var conds []seccomp.ScmpCondition + for pos, arg := range tokens[1:] { + var cmpOp seccomp.ScmpCompareOp + var value uint64 + var err error + + if arg == "-" { // skip arg + continue + } + + if strings.HasPrefix(arg, ">=") { + cmpOp = seccomp.CompareGreaterEqual + value, err = readNumber(arg[2:]) + } else if strings.HasPrefix(arg, "<=") { + cmpOp = seccomp.CompareLessOrEqual + value, err = readNumber(arg[2:]) + } else if strings.HasPrefix(arg, "!") { + cmpOp = seccomp.CompareNotEqual + value, err = readNumber(arg[1:]) + } else if strings.HasPrefix(arg, "<") { + cmpOp = seccomp.CompareLess + value, err = readNumber(arg[1:]) + } else if strings.HasPrefix(arg, ">") { + cmpOp = seccomp.CompareGreater + value, err = readNumber(arg[1:]) + } else if strings.HasPrefix(arg, "|") { + cmpOp = seccomp.CompareMaskedEqual + value, err = readNumber(arg[1:]) + } else if strings.HasPrefix(arg, "u:") { + cmpOp = seccomp.CompareEqual + value, err = findUid(arg[2:]) + if err != nil { + return fmt.Errorf("cannot parse token %q (line %q): %v", arg, line, err) + } + } else if strings.HasPrefix(arg, "g:") { + cmpOp = seccomp.CompareEqual + value, err = findGid(arg[2:]) + if err != nil { + return fmt.Errorf("cannot parse token %q (line %q): %v", arg, line, err) + } + } else { + cmpOp = seccomp.CompareEqual + value, err = readNumber(arg) + } + if err != nil { + return fmt.Errorf("cannot parse token %q (line %q)", arg, line) + } + + var scmpCond seccomp.ScmpCondition + if cmpOp == seccomp.CompareMaskedEqual { + scmpCond, err = seccomp.MakeCondition(uint(pos), cmpOp, value, value) + } else { + scmpCond, err = seccomp.MakeCondition(uint(pos), cmpOp, value) + } + if err != nil { + return fmt.Errorf("cannot parse line %q: %s", line, err) + } + conds = append(conds, scmpCond) + } + + // Default to adding a precise match if possible. Otherwise + // let seccomp figure out the architecture specifics. + if err = secFilter.AddRuleConditionalExact(secSyscall, seccomp.ActAllow, conds); err != nil { + err = secFilter.AddRuleConditional(secSyscall, seccomp.ActAllow, conds) + } + + return err +} + +// used to mock in tests +var ( + archUbuntuArchitecture = arch.UbuntuArchitecture + archUbuntuKernelArchitecture = arch.UbuntuKernelArchitecture +) + +var ( + ubuntuArchitecture = archUbuntuArchitecture() + ubuntuKernelArchitecture = archUbuntuKernelArchitecture() +) + +// For architectures that support a compat architecture, when the +// kernel and userspace match, add the compat arch, otherwise add +// the kernel arch to support the kernel's arch (eg, 64bit kernels with +// 32bit userspace). +func addSecondaryArches(secFilter *seccomp.ScmpFilter) error { + // note that all architecture strings are in the dpkg + // architecture notation + var compatArch seccomp.ScmpArch + + // common case: kernel and userspace have the same arch. We + // add a compat architecture for some architectures that + // support it, e.g. on amd64 kernel and userland, we add + // compat i386 syscalls. + if ubuntuArchitecture == ubuntuKernelArchitecture { + switch archUbuntuArchitecture() { + case "amd64": + compatArch = seccomp.ArchX86 + case "arm64": + compatArch = seccomp.ArchARM + case "ppc64": + compatArch = seccomp.ArchPPC + } + } else { + // less common case: kernel and userspace have different archs + // so add a compat architecture that matches the kernel. E.g. + // an amd64 kernel with i386 userland needs the amd64 secondary + // arch added to support specialized snaps that might + // conditionally call 64bit code when the kernel supports it. + // Note that in this case snapd requests i386 (or arch 'all') + // snaps. While unusual from a traditional Linux distribution + // perspective, certain classes of embedded devices are known + // to use this configuration. + compatArch = UbuntuArchToScmpArch(archUbuntuKernelArchitecture()) + } + + if compatArch != seccomp.ArchInvalid { + return secFilter.AddArch(compatArch) + } + + return nil +} + +func compile(content []byte, out string) error { + var err error + var secFilter *seccomp.ScmpFilter + + secFilter, err = seccomp.NewFilter(seccomp.ActKill) + if err != nil { + return fmt.Errorf("cannot create seccomp filter: %s", err) + } + + if err := addSecondaryArches(secFilter); err != nil { + return err + } + + scanner := bufio.NewScanner(bytes.NewBuffer(content)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // special case: unrestricted means we stop early, we just + // write this special tag and evalulate in snap-confine + if line == "@unrestricted" { + return osutil.AtomicWrite(out, bytes.NewBufferString(line+"\n"), 0644, 0) + } + // complain mode is a "allow-all" filter for now until + // we can land https://github.com/snapcore/snapd/pull/3998 + if line == "@complain" { + secFilter, err = seccomp.NewFilter(seccomp.ActAllow) + if err != nil { + return fmt.Errorf("cannot create seccomp filter: %s", err) + } + if err := addSecondaryArches(secFilter); err != nil { + return err + } + break + } + + // look for regular syscall/arg rule + if err := parseLine(line, secFilter); err != nil { + return fmt.Errorf("cannot parse line: %s", err) + } + } + if scanner.Err(); err != nil { + return err + } + + if osutil.GetenvBool("SNAP_SECCOMP_DEBUG") { + secFilter.ExportPFC(os.Stdout) + } + + // write atomically + fout, err := osutil.NewAtomicFile(out, 0644, 0, -1, -1) + if err != nil { + return err + } + defer fout.Close() + + if err := secFilter.ExportBPF(fout.File); err != nil { + return err + } + return fout.Commit() +} + +// Be very strict so usernames and groups specified in policy are widely +// compatible. From NAME_REGEX in /etc/adduser.conf +var userGroupNamePattern = regexp.MustCompile("^[a-z][-a-z0-9_]*$") + +// findUid returns the identifier of the given UNIX user name. +func findUid(username string) (uint64, error) { + if !userGroupNamePattern.MatchString(username) { + return 0, fmt.Errorf("%q must be a valid username", username) + } + return osutil.FindUid(username) +} + +// findGid returns the identifier of the given UNIX group name. +func findGid(group string) (uint64, error) { + if !userGroupNamePattern.MatchString(group) { + return 0, fmt.Errorf("%q must be a valid group name", group) + } + return osutil.FindGid(group) +} + +func showSeccompLibraryVersion() error { + major, minor, micro := seccomp.GetLibraryVersion() + fmt.Fprintf(os.Stdout, "%d.%d.%d\n", major, minor, micro) + return nil +} + +func main() { + var err error + var content []byte + + if len(os.Args) < 2 { + fmt.Printf("%s: need a command\n", os.Args[0]) + os.Exit(1) + } + + cmd := os.Args[1] + switch cmd { + case "compile": + if len(os.Args) < 4 { + fmt.Println("compile needs an input and output file") + os.Exit(1) + } + content, err = ioutil.ReadFile(os.Args[2]) + if err != nil { + break + } + err = compile(content, os.Args[3]) + case "library-version": + err = showSeccompLibraryVersion() + default: + err = fmt.Errorf("unsupported argument %q", cmd) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/snap-seccomp/main_ppc64le.go b/cmd/snap-seccomp/main_ppc64le.go new file mode 100644 index 00000000..fdfc2543 --- /dev/null +++ b/cmd/snap-seccomp/main_ppc64le.go @@ -0,0 +1,31 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +// +build ppc64le,go1.7,!go1.8 + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +/* +#cgo LDFLAGS: -no-pie + +// we need "-no-pie" for ppc64le,go1.7 to work around build failure on +// ppc64el with go1.7, see +// https://forum.snapcraft.io/t/snapd-master-fails-on-zesty-ppc64el-with-r-ppc64-addr16-ha-for-symbol-out-of-range/ +*/ +import "C" diff --git a/cmd/snap-seccomp/main_test.go b/cmd/snap-seccomp/main_test.go new file mode 100644 index 00000000..d83b6169 --- /dev/null +++ b/cmd/snap-seccomp/main_test.go @@ -0,0 +1,794 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + . "gopkg.in/check.v1" + + "github.com/mvo5/libseccomp-golang" + + "github.com/snapcore/snapd/arch" + main "github.com/snapcore/snapd/cmd/snap-seccomp" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type snapSeccompSuite struct { + seccompBpfLoader string + seccompSyscallRunner string +} + +var _ = Suite(&snapSeccompSuite{}) + +var seccompBpfLoaderContent = []byte(` +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define MAX_BPF_SIZE 32 * 1024 + +int sc_apply_seccomp_bpf(const char* profile_path) +{ + unsigned char bpf[MAX_BPF_SIZE + 1]; // account for EOF + FILE* fp; + fp = fopen(profile_path, "rb"); + if (fp == NULL) { + fprintf(stderr, "cannot read %s\n", profile_path); + return -1; + } + + // set 'size' to 1; to get bytes transferred + size_t num_read = fread(bpf, 1, sizeof(bpf), fp); + + if (ferror(fp) != 0) { + perror("fread()"); + return -1; + } else if (feof(fp) == 0) { + fprintf(stderr, "file too big\n"); + return -1; + } + fclose(fp); + + struct sock_fprog prog = { + .len = num_read / sizeof(struct sock_filter), + .filter = (struct sock_filter*)bpf, + }; + + // Set NNP to allow loading seccomp policy into the kernel without + // root + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("prctl(PR_NO_NEW_PRIVS, 1, 0, 0, 0)"); + return -1; + } + + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) { + perror("prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...) failed"); + return -1; + } + return 0; +} + +int main(int argc, char* argv[]) +{ + int rc = 0; + if (argc < 2) { + fprintf(stderr, "Usage: %s [prog ...]\n", argv[0]); + return 1; + } + + rc = sc_apply_seccomp_bpf(argv[1]); + if (rc != 0) + return -rc; + + execv(argv[2], (char* const*)&argv[2]); + perror("execv failed"); + return 1; +} +`) + +var seccompSyscallRunnerContent = []byte(` +#define _GNU_SOURCE +#include +#include +#include +int main(int argc, char** argv) +{ + int l[7]; + for (int i = 0; i < 7; i++) + l[i] = atoi(argv[i + 1]); + // There might be architecture-specific requirements. see "man syscall" + // for details. + syscall(l[0], l[1], l[2], l[3], l[4], l[5], l[6]); + syscall(SYS_exit, 0, 0, 0, 0, 0, 0); + return 0; +} +`) + +func lastKmsg() string { + output, err := exec.Command("dmesg").CombinedOutput() + if err != nil { + return err.Error() + } + l := strings.Split(string(output), "\n") + return fmt.Sprintf("Showing last 10 lines of dmesg:\n%s", strings.Join(l[len(l)-10:], "\n")) +} + +func (s *snapSeccompSuite) SetUpSuite(c *C) { + // build seccomp-load helper + s.seccompBpfLoader = filepath.Join(c.MkDir(), "seccomp_bpf_loader") + err := ioutil.WriteFile(s.seccompBpfLoader+".c", seccompBpfLoaderContent, 0644) + c.Assert(err, IsNil) + cmd := exec.Command("gcc", "-Werror", "-Wall", s.seccompBpfLoader+".c", "-o", s.seccompBpfLoader) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + c.Assert(err, IsNil) + + // build syscall-runner helper + s.seccompSyscallRunner = filepath.Join(c.MkDir(), "seccomp_syscall_runner") + err = ioutil.WriteFile(s.seccompSyscallRunner+".c", seccompSyscallRunnerContent, 0644) + c.Assert(err, IsNil) + + cmd = exec.Command("gcc", "-std=c99", "-Werror", "-Wall", "-static", s.seccompSyscallRunner+".c", "-o", s.seccompSyscallRunner, "-Wl,-static", "-static-libgcc") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + c.Assert(err, IsNil) + + // Build 32bit runner on amd64 to test non-native syscall handling. + // Ideally we would build for ppc64el->powerpc and arm64->armhf but + // it seems tricky to find the right gcc-multilib for this. + if arch.UbuntuArchitecture() == "amd64" { + cmd = exec.Command(cmd.Args[0], cmd.Args[1:]...) + cmd.Args = append(cmd.Args, "-m32") + for i, k := range cmd.Args { + if k == s.seccompSyscallRunner { + cmd.Args[i] = s.seccompSyscallRunner + ".m32" + } + } + if output, err := cmd.CombinedOutput(); err != nil { + fmt.Printf("cannot build multi-lib syscall runner: %v\n%s", err, output) + } + } +} + +// Runs the policy through the kernel: +// 1. runs main.Compile() +// 2. the program in seccompBpfLoaderContent with the output file as an +// argument +// 3. the program in seccompBpfLoaderContent loads the output file BPF into +// the kernel and executes the program in seccompBpfRunnerContent with the +// syscall and arguments specified by the test +// +// In this manner, in addition to verifying policy syntax we are able to +// unit test the resulting bpf in several ways. +// +// Full testing of applied policy is done elsewhere via spread tests. +// +// Note that we skip testing prctl(PR_SET_ENDIAN) - it causes havoc when +// it is run. We will also need to skip: fadvise64_64, +// ftruncate64, posix_fadvise, pread64, pwrite64, readahead, +// sync_file_range, and truncate64. +// Once we start using those. See `man syscall` +func (s *snapSeccompSuite) runBpf(c *C, seccompWhitelist, bpfInput string, expected int) { + // Common syscalls we need to allow for a minimal statically linked + // c program. + // + // If we compile a test program for each test we can get away with + // a even smaller set of syscalls: execve,exit essentially. But it + // means a much longer test run (30s vs 2s). Commit d288d89 contains + // the code for this. + common := ` +execve +uname +brk +arch_prctl +readlink +access +sysinfo +exit +# i386 +set_thread_area +# armhf +set_tls +# arm64 +readlinkat +faccessat +# i386 from amd64 +restart_syscall +` + bpfPath := filepath.Join(c.MkDir(), "bpf") + err := main.Compile([]byte(common+seccompWhitelist), bpfPath) + c.Assert(err, IsNil) + + // default syscall runner + syscallRunner := s.seccompSyscallRunner + + // syscallName;arch;arg1,arg2... + l := strings.Split(bpfInput, ";") + syscallName := l[0] + syscallArch := "native" + if len(l) > 1 { + syscallArch = l[1] + } + + syscallNr, err := seccomp.GetSyscallFromName(syscallName) + c.Assert(err, IsNil) + + // Check if we want to test non-native architecture + // handling. Doing this via the in-kernel tests is tricky as + // we need a kernel that can run the architecture and a + // compiler that can produce the required binaries. Currently + // we only test amd64 running i386 here. + if syscallArch != "native" { + syscallNr, err = seccomp.GetSyscallFromNameByArch(syscallName, main.UbuntuArchToScmpArch(syscallArch)) + c.Assert(err, IsNil) + + switch syscallArch { + case "amd64": + // default syscallRunner + case "i386": + syscallRunner = s.seccompSyscallRunner + ".m32" + default: + c.Errorf("unexpected non-native arch: %s", syscallArch) + } + } + switch { + case syscallNr == -101: + // "socket" + // see libseccomp: _s390x_sock_demux(), _x86_sock_demux() + // the -101 is translated to 359 (socket) + syscallNr = 359 + case syscallNr == -10165: + // "mknod" on arm64 is not available at all on arm64 + // only "mknodat" but libseccomp will not generate a + // "mknodat" whitelist, it geneates a whitelist with + // syscall -10165 (!?!) so we cannot test this. + c.Skip("skipping mknod tests on arm64") + case syscallNr < 0: + c.Errorf("failed to resolve %v: %v", l[0], syscallNr) + return + } + + var syscallRunnerArgs [7]string + syscallRunnerArgs[0] = strconv.FormatInt(int64(syscallNr), 10) + if len(l) > 2 { + args := strings.Split(l[2], ",") + for i := range args { + // init with random number argument + syscallArg := (uint64)(rand.Uint32()) + // override if the test specifies a specific number + if nr, err := strconv.ParseUint(args[i], 10, 64); err == nil { + syscallArg = nr + } else if nr, ok := main.SeccompResolver[args[i]]; ok { + syscallArg = nr + } + syscallRunnerArgs[i+1] = strconv.FormatUint(syscallArg, 10) + } + } + + cmd := exec.Command(s.seccompBpfLoader, bpfPath, syscallRunner, syscallRunnerArgs[0], syscallRunnerArgs[1], syscallRunnerArgs[2], syscallRunnerArgs[3], syscallRunnerArgs[4], syscallRunnerArgs[5], syscallRunnerArgs[6]) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + switch expected { + case main.SeccompRetAllow: + if err != nil { + c.Fatalf("unexpected error for %q (failed to run %q): %s", seccompWhitelist, lastKmsg(), err) + } + case main.SeccompRetKill: + if err == nil { + c.Fatalf("unexpected success for %q %q (ran but should have failed %s)", seccompWhitelist, bpfInput, lastKmsg()) + } + default: + c.Fatalf("unknown expected result %v", expected) + } +} + +func (s *snapSeccompSuite) TestUnrestricted(c *C) { + inp := "@unrestricted\n" + outPath := filepath.Join(c.MkDir(), "bpf") + err := main.Compile([]byte(inp), outPath) + c.Assert(err, IsNil) + + content, err := ioutil.ReadFile(outPath) + c.Assert(err, IsNil) + c.Check(content, DeepEquals, []byte(inp)) + +} + +// TestCompile iterates over a range of textual seccomp whitelist rules and +// mocked kernel syscall input. For each rule, the test consists of compiling +// the rule into a bpf program and then running that program on a virtual bpf +// machine and comparing the bpf machine output to the specified expected +// output and seccomp operation. Eg: +// {"", "", } +// +// Eg to test that the rule 'read >=2' is allowed with 'read(2)' and 'read(3)' +// and denied with 'read(1)' and 'read(0)', add the following tests: +// {"read >=2", "read;native;2", main.SeccompRetAllow}, +// {"read >=2", "read;native;3", main.SeccompRetAllow}, +// {"read >=2", "read;native;1", main.SeccompRetKill}, +// {"read >=2", "read;native;0", main.SeccompRetKill}, +func (s *snapSeccompSuite) TestCompile(c *C) { + + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // special + {"@complain", "execve", main.SeccompRetAllow}, + + // trivial allow + {"read", "read", main.SeccompRetAllow}, + {"read\nwrite\nexecve\n", "write", main.SeccompRetAllow}, + + // trivial denial + {"read", "ioctl", main.SeccompRetKill}, + + // test argument filtering syntax, we currently support: + // >=, <=, !, <, >, | + // modifiers. + + // reads >= 2 are ok + {"read >=2", "read;native;2", main.SeccompRetAllow}, + {"read >=2", "read;native;3", main.SeccompRetAllow}, + // but not reads < 2, those get killed + {"read >=2", "read;native;1", main.SeccompRetKill}, + {"read >=2", "read;native;0", main.SeccompRetKill}, + + // reads <= 2 are ok + {"read <=2", "read;native;0", main.SeccompRetAllow}, + {"read <=2", "read;native;1", main.SeccompRetAllow}, + {"read <=2", "read;native;2", main.SeccompRetAllow}, + // but not reads >2, those get killed + {"read <=2", "read;native;3", main.SeccompRetKill}, + {"read <=2", "read;native;4", main.SeccompRetKill}, + + // reads that are not 2 are ok + {"read !2", "read;native;1", main.SeccompRetAllow}, + {"read !2", "read;native;3", main.SeccompRetAllow}, + // but not 2, this gets killed + {"read !2", "read;native;2", main.SeccompRetKill}, + + // reads > 2 are ok + {"read >2", "read;native;4", main.SeccompRetAllow}, + {"read >2", "read;native;3", main.SeccompRetAllow}, + // but not reads <= 2, those get killed + {"read >2", "read;native;2", main.SeccompRetKill}, + {"read >2", "read;native;1", main.SeccompRetKill}, + + // reads < 2 are ok + {"read <2", "read;native;0", main.SeccompRetAllow}, + {"read <2", "read;native;1", main.SeccompRetAllow}, + // but not reads >= 2, those get killed + {"read <2", "read;native;2", main.SeccompRetKill}, + {"read <2", "read;native;3", main.SeccompRetKill}, + + // FIXME: test maskedEqual better + {"read |1", "read;native;1", main.SeccompRetAllow}, + {"read |1", "read;native;2", main.SeccompRetKill}, + + // exact match, reads == 2 are ok + {"read 2", "read;native;2", main.SeccompRetAllow}, + // but not those != 2 + {"read 2", "read;native;3", main.SeccompRetKill}, + {"read 2", "read;native;1", main.SeccompRetKill}, + + // test actual syscalls and their expected usage + {"ioctl - TIOCSTI", "ioctl;native;-,TIOCSTI", main.SeccompRetAllow}, + {"ioctl - TIOCSTI", "ioctl;native;-,99", main.SeccompRetKill}, + {"ioctl - !TIOCSTI", "ioctl;native;-,TIOCSTI", main.SeccompRetKill}, + + // test_bad_seccomp_filter_args_clone + {"setns - CLONE_NEWNET", "setns;native;-,99", main.SeccompRetKill}, + {"setns - CLONE_NEWNET", "setns;native;-,CLONE_NEWNET", main.SeccompRetAllow}, + + // test_bad_seccomp_filter_args_mknod + {"mknod - |S_IFIFO", "mknod;native;-,S_IFIFO", main.SeccompRetAllow}, + {"mknod - |S_IFIFO", "mknod;native;-,99", main.SeccompRetKill}, + + // test_bad_seccomp_filter_args_prctl + {"prctl PR_CAP_AMBIENT_RAISE", "prctl;native;PR_CAP_AMBIENT_RAISE", main.SeccompRetAllow}, + {"prctl PR_CAP_AMBIENT_RAISE", "prctl;native;99", main.SeccompRetKill}, + + // test_bad_seccomp_filter_args_prio + {"setpriority PRIO_PROCESS 0 >=0", "setpriority;native;PRIO_PROCESS,0,19", main.SeccompRetAllow}, + {"setpriority PRIO_PROCESS 0 >=0", "setpriority;native;99", main.SeccompRetKill}, + + // test_bad_seccomp_filter_args_quotactl + {"quotactl Q_GETQUOTA", "quotactl;native;Q_GETQUOTA", main.SeccompRetAllow}, + {"quotactl Q_GETQUOTA", "quotactl;native;99", main.SeccompRetKill}, + + // test_bad_seccomp_filter_args_termios + {"ioctl - TIOCSTI", "ioctl;native;-,TIOCSTI", main.SeccompRetAllow}, + {"ioctl - TIOCSTI", "ioctl;native;-,99", main.SeccompRetKill}, + + // u:root g:root + {"fchown - u:root g:root", "fchown;native;-,0,0", main.SeccompRetAllow}, + {"fchown - u:root g:root", "fchown;native;-,99,0", main.SeccompRetKill}, + {"chown - u:root g:root", "chown;native;-,0,0", main.SeccompRetAllow}, + {"chown - u:root g:root", "chown;native;-,99,0", main.SeccompRetKill}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +// TestCompileSocket runs in a separate tests so that only this part +// can be skipped when "socketcall()" is used instead of "socket()". +// +// Some architectures (i386, s390x) use the "socketcall" syscall instead +// of "socket". This is the case on Ubuntu 14.04, 17.04, 17.10 +func (s *snapSeccompSuite) TestCompileSocket(c *C) { + if release.ReleaseInfo.ID == "ubuntu" && release.ReleaseInfo.VersionID == "14.04" { + c.Skip("14.04/i386 uses socketcall which cannot be tested here") + } + + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + + // test_bad_seccomp_filter_args_socket + {"socket AF_UNIX", "socket;native;AF_UNIX", main.SeccompRetAllow}, + {"socket AF_UNIX", "socket;native;99", main.SeccompRetKill}, + {"socket - SOCK_STREAM", "socket;native;-,SOCK_STREAM", main.SeccompRetAllow}, + {"socket - SOCK_STREAM", "socket;native;-,99", main.SeccompRetKill}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } + +} + +func (s *snapSeccompSuite) TestCompileBadInput(c *C) { + for _, t := range []struct { + inp string + errMsg string + }{ + // test_bad_seccomp_filter_args_clone (various typos in input) + {"setns - CLONE_NEWNE", `cannot parse line: cannot parse token "CLONE_NEWNE" \(line "setns - CLONE_NEWNE"\)`}, + {"setns - CLONE_NEWNETT", `cannot parse line: cannot parse token "CLONE_NEWNETT" \(line "setns - CLONE_NEWNETT"\)`}, + {"setns - CL0NE_NEWNET", `cannot parse line: cannot parse token "CL0NE_NEWNET" \(line "setns - CL0NE_NEWNET"\)`}, + + // test_bad_seccomp_filter_args_mknod (various typos in input) + {"mknod - |S_IFIF", `cannot parse line: cannot parse token "S_IFIF" \(line "mknod - |S_IFIF"\)`}, + {"mknod - |S_IFIFOO", `cannot parse line: cannot parse token "S_IFIFOO" \(line "mknod - |S_IFIFOO"\)`}, + {"mknod - |S_!FIFO", `cannot parse line: cannot parse token "S_IFIFO" \(line "mknod - |S_!FIFO"\)`}, + + // test_bad_seccomp_filter_args_null + {"socket S\x00CK_STREAM", `cannot parse line: cannot parse token .*`}, + {"socket SOCK_STREAM\x00bad stuff", `cannot parse line: cannot parse token .*`}, + + // test_bad_seccomp_filter_args + {"setpriority bar", `cannot parse line: cannot parse token "bar" .*`}, + {"setpriority -1", `cannot parse line: cannot parse token "-1" .*`}, + {"setpriority 0 - -1 0", `cannot parse line: cannot parse token "-1" .*`}, + {"setpriority --10", `cannot parse line: cannot parse token "--10" .*`}, + {"setpriority 0:10", `cannot parse line: cannot parse token "0:10" .*`}, + {"setpriority 0-10", `cannot parse line: cannot parse token "0-10" .*`}, + {"setpriority 0,1", `cannot parse line: cannot parse token "0,1" .*`}, + {"setpriority 0x0", `cannot parse line: cannot parse token "0x0" .*`}, + {"setpriority a1", `cannot parse line: cannot parse token "a1" .*`}, + {"setpriority 1a", `cannot parse line: cannot parse token "1a" .*`}, + {"setpriority 1-", `cannot parse line: cannot parse token "1-" .*`}, + {"setpriority 1\\ 2", `cannot parse line: cannot parse token "1\\\\" .*`}, + {"setpriority 1\\n2", `cannot parse line: cannot parse token "1\\\\n2" .*`}, + {"setpriority 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", `cannot parse line: cannot parse token "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" .*`}, + {"mbind - - - - - - 7", `cannot parse line: too many arguments specified for syscall 'mbind' in line.*`}, + {"mbind 1 2 3 4 5 6 7", `cannot parse line: too many arguments specified for syscall 'mbind' in line.*`}, + // test_bad_seccomp_filter_args_prctl + {"prctl PR_GET_SECCOM", `cannot parse line: cannot parse token "PR_GET_SECCOM" .*`}, + {"prctl PR_GET_SECCOMPP", `cannot parse line: cannot parse token "PR_GET_SECCOMPP" .*`}, + {"prctl PR_GET_SECC0MP", `cannot parse line: cannot parse token "PR_GET_SECC0MP" .*`}, + {"prctl PR_CAP_AMBIENT_RAIS", `cannot parse line: cannot parse token "PR_CAP_AMBIENT_RAIS" .*`}, + {"prctl PR_CAP_AMBIENT_RAISEE", `cannot parse line: cannot parse token "PR_CAP_AMBIENT_RAISEE" .*`}, + // test_bad_seccomp_filter_args_prio + {"setpriority PRIO_PROCES 0 >=0", `cannot parse line: cannot parse token "PRIO_PROCES" .*`}, + {"setpriority PRIO_PROCESSS 0 >=0", `cannot parse line: cannot parse token "PRIO_PROCESSS" .*`}, + {"setpriority PRIO_PR0CESS 0 >=0", `cannot parse line: cannot parse token "PRIO_PR0CESS" .*`}, + // test_bad_seccomp_filter_args_quotactl + {"quotactl Q_GETQUOT", `cannot parse line: cannot parse token "Q_GETQUOT" .*`}, + {"quotactl Q_GETQUOTAA", `cannot parse line: cannot parse token "Q_GETQUOTAA" .*`}, + {"quotactl Q_GETQU0TA", `cannot parse line: cannot parse token "Q_GETQU0TA" .*`}, + // test_bad_seccomp_filter_args_socket + {"socket AF_UNI", `cannot parse line: cannot parse token "AF_UNI" .*`}, + {"socket AF_UNIXX", `cannot parse line: cannot parse token "AF_UNIXX" .*`}, + {"socket AF_UN!X", `cannot parse line: cannot parse token "AF_UN!X" .*`}, + {"socket - SOCK_STREA", `cannot parse line: cannot parse token "SOCK_STREA" .*`}, + {"socket - SOCK_STREAMM", `cannot parse line: cannot parse token "SOCK_STREAMM" .*`}, + {"socket - NETLINK_ROUT", `cannot parse line: cannot parse token "NETLINK_ROUT" .*`}, + {"socket - NETLINK_ROUTEE", `cannot parse line: cannot parse token "NETLINK_ROUTEE" .*`}, + {"socket - NETLINK_R0UTE", `cannot parse line: cannot parse token "NETLINK_R0UTE" .*`}, + // test_bad_seccomp_filter_args_termios + {"ioctl - TIOCST", `cannot parse line: cannot parse token "TIOCST" .*`}, + {"ioctl - TIOCSTII", `cannot parse line: cannot parse token "TIOCSTII" .*`}, + {"ioctl - TIOCST1", `cannot parse line: cannot parse token "TIOCST1" .*`}, + // ensure missing numbers are caught + {"setpriority >", `cannot parse line: cannot parse token ">" .*`}, + {"setpriority >=", `cannot parse line: cannot parse token ">=" .*`}, + {"setpriority <", `cannot parse line: cannot parse token "<" .*`}, + {"setpriority <=", `cannot parse line: cannot parse token "<=" .*`}, + {"setpriority |", `cannot parse line: cannot parse token "|" .*`}, + {"setpriority !", `cannot parse line: cannot parse token "!" .*`}, + + // u: + {"setuid :root", `cannot parse line: cannot parse token ":root" .*`}, + {"setuid u:", `cannot parse line: cannot parse token "u:" \(line "setuid u:"\): "" must be a valid username`}, + {"setuid u:0", `cannot parse line: cannot parse token "u:0" \(line "setuid u:0"\): "0" must be a valid username`}, + {"setuid u:b@d|npu+", `cannot parse line: cannot parse token "u:b@d|npu+" \(line "setuid u:b@d|npu+"\): "b@d|npu+" must be a valid username`}, + {"setuid u:snap.bad", `cannot parse line: cannot parse token "u:snap.bad" \(line "setuid u:snap.bad"\): "snap.bad" must be a valid username`}, + {"setuid U:root", `cannot parse line: cannot parse token "U:root" .*`}, + {"setuid u:nonexistent", `cannot parse line: cannot parse token "u:nonexistent" \(line "setuid u:nonexistent"\): user: unknown user nonexistent`}, + // g: + {"setgid g:", `cannot parse line: cannot parse token "g:" \(line "setgid g:"\): "" must be a valid group name`}, + {"setgid g:0", `cannot parse line: cannot parse token "g:0" \(line "setgid g:0"\): "0" must be a valid group name`}, + {"setgid g:b@d|npu+", `cannot parse line: cannot parse token "g:b@d|npu+" \(line "setgid g:b@d|npu+"\): "b@d|npu+" must be a valid group name`}, + {"setgid g:snap.bad", `cannot parse line: cannot parse token "g:snap.bad" \(line "setgid g:snap.bad"\): "snap.bad" must be a valid group name`}, + {"setgid G:root", `cannot parse line: cannot parse token "G:root" .*`}, + {"setgid g:nonexistent", `cannot parse line: cannot parse token "g:nonexistent" \(line "setgid g:nonexistent"\): group: unknown group nonexistent`}, + } { + outPath := filepath.Join(c.MkDir(), "bpf") + err := main.Compile([]byte(t.inp), outPath) + c.Check(err, ErrorMatches, t.errMsg, Commentf("%q errors in unexpected ways, got: %q expected %q", t.inp, err, t.errMsg)) + } +} + +// ported from test_restrictions_working_args_socket +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsSocket(c *C) { + if release.ReleaseInfo.ID == "ubuntu" && release.ReleaseInfo.VersionID == "14.04" { + c.Skip("14.04/i386 uses socketcall which cannot be tested here") + } + + for _, pre := range []string{"AF", "PF"} { + for _, i := range []string{"UNIX", "LOCAL", "INET", "INET6", "IPX", "NETLINK", "X25", "AX25", "ATMPVC", "APPLETALK", "PACKET", "ALG", "CAN", "BRIDGE", "NETROM", "ROSE", "NETBEUI", "SECURITY", "KEY", "ASH", "ECONET", "SNA", "IRDA", "PPPOX", "WANPIPE", "BLUETOOTH", "RDS", "LLC", "TIPC", "IUCV", "RXRPC", "ISDN", "PHONET", "IEEE802154", "CAIF", "NFC", "VSOCK", "MPLS", "IB"} { + seccompWhitelist := fmt.Sprintf("socket %s_%s", pre, i) + bpfInputGood := fmt.Sprintf("socket;native;%s_%s", pre, i) + bpfInputBad := "socket;native;99999" + s.runBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + s.runBpf(c, seccompWhitelist, bpfInputBad, main.SeccompRetKill) + + for _, j := range []string{"SOCK_STREAM", "SOCK_DGRAM", "SOCK_SEQPACKET", "SOCK_RAW", "SOCK_RDM", "SOCK_PACKET"} { + seccompWhitelist := fmt.Sprintf("socket %s_%s %s", pre, i, j) + bpfInputGood := fmt.Sprintf("socket;native;%s_%s,%s", pre, i, j) + bpfInputBad := fmt.Sprintf("socket;native;%s_%s,9999", pre, i) + s.runBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + s.runBpf(c, seccompWhitelist, bpfInputBad, main.SeccompRetKill) + } + } + } + + for _, i := range []string{"NETLINK_ROUTE", "NETLINK_USERSOCK", "NETLINK_FIREWALL", "NETLINK_SOCK_DIAG", "NETLINK_NFLOG", "NETLINK_XFRM", "NETLINK_SELINUX", "NETLINK_ISCSI", "NETLINK_AUDIT", "NETLINK_FIB_LOOKUP", "NETLINK_CONNECTOR", "NETLINK_NETFILTER", "NETLINK_IP6_FW", "NETLINK_DNRTMSG", "NETLINK_KOBJECT_UEVENT", "NETLINK_GENERIC", "NETLINK_SCSITRANSPORT", "NETLINK_ECRYPTFS", "NETLINK_RDMA", "NETLINK_CRYPTO", "NETLINK_INET_DIAG"} { + for _, j := range []string{"AF_NETLINK", "PF_NETLINK"} { + seccompWhitelist := fmt.Sprintf("socket %s - %s", j, i) + bpfInputGood := fmt.Sprintf("socket;native;%s,0,%s", j, i) + bpfInputBad := fmt.Sprintf("socket;native;%s,0,99", j) + s.runBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + s.runBpf(c, seccompWhitelist, bpfInputBad, main.SeccompRetKill) + } + } +} + +// ported from test_restrictions_working_args_quotactl +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsQuotactl(c *C) { + for _, arg := range []string{"Q_QUOTAON", "Q_QUOTAOFF", "Q_GETQUOTA", "Q_SETQUOTA", "Q_GETINFO", "Q_SETINFO", "Q_GETFMT", "Q_SYNC", "Q_XQUOTAON", "Q_XQUOTAOFF", "Q_XGETQUOTA", "Q_XSETQLIM", "Q_XGETQSTAT", "Q_XQUOTARM"} { + // good input + seccompWhitelist := fmt.Sprintf("quotactl %s", arg) + bpfInputGood := fmt.Sprintf("quotactl;native;%s", arg) + s.runBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + // bad input + for _, bad := range []string{"quotactl;native;99999", "read;native;"} { + s.runBpf(c, seccompWhitelist, bad, main.SeccompRetKill) + } + } +} + +// ported from test_restrictions_working_args_prctl +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsPrctl(c *C) { + for _, arg := range []string{"PR_CAP_AMBIENT", "PR_CAP_AMBIENT_RAISE", "PR_CAP_AMBIENT_LOWER", "PR_CAP_AMBIENT_IS_SET", "PR_CAP_AMBIENT_CLEAR_ALL", "PR_CAPBSET_READ", "PR_CAPBSET_DROP", "PR_SET_CHILD_SUBREAPER", "PR_GET_CHILD_SUBREAPER", "PR_SET_DUMPABLE", "PR_GET_DUMPABLE", "PR_GET_ENDIAN", "PR_SET_FPEMU", "PR_GET_FPEMU", "PR_SET_FPEXC", "PR_GET_FPEXC", "PR_SET_KEEPCAPS", "PR_GET_KEEPCAPS", "PR_MCE_KILL", "PR_MCE_KILL_GET", "PR_SET_MM", "PR_SET_MM_START_CODE", "PR_SET_MM_END_CODE", "PR_SET_MM_START_DATA", "PR_SET_MM_END_DATA", "PR_SET_MM_START_STACK", "PR_SET_MM_START_BRK", "PR_SET_MM_BRK", "PR_SET_MM_ARG_START", "PR_SET_MM_ARG_END", "PR_SET_MM_ENV_START", "PR_SET_MM_ENV_END", "PR_SET_MM_AUXV", "PR_SET_MM_EXE_FILE", "PR_MPX_ENABLE_MANAGEMENT", "PR_MPX_DISABLE_MANAGEMENT", "PR_SET_NAME", "PR_GET_NAME", "PR_SET_NO_NEW_PRIVS", "PR_GET_NO_NEW_PRIVS", "PR_SET_PDEATHSIG", "PR_GET_PDEATHSIG", "PR_SET_PTRACER", "PR_SET_SECCOMP", "PR_GET_SECCOMP", "PR_SET_SECUREBITS", "PR_GET_SECUREBITS", "PR_SET_THP_DISABLE", "PR_TASK_PERF_EVENTS_DISABLE", "PR_TASK_PERF_EVENTS_ENABLE", "PR_GET_THP_DISABLE", "PR_GET_TID_ADDRESS", "PR_SET_TIMERSLACK", "PR_GET_TIMERSLACK", "PR_SET_TIMING", "PR_GET_TIMING", "PR_SET_TSC", "PR_GET_TSC", "PR_SET_UNALIGN", "PR_GET_UNALIGN"} { + // good input + seccompWhitelist := fmt.Sprintf("prctl %s", arg) + bpfInputGood := fmt.Sprintf("prctl;native;%s", arg) + s.runBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + // bad input + for _, bad := range []string{"prctl;native;99999", "setpriority;native;"} { + s.runBpf(c, seccompWhitelist, bad, main.SeccompRetKill) + } + + if arg == "PR_CAP_AMBIENT" { + for _, j := range []string{"PR_CAP_AMBIENT_RAISE", "PR_CAP_AMBIENT_LOWER", "PR_CAP_AMBIENT_IS_SET", "PR_CAP_AMBIENT_CLEAR_ALL"} { + seccompWhitelist := fmt.Sprintf("prctl %s %s", arg, j) + bpfInputGood := fmt.Sprintf("prctl;native;%s,%s", arg, j) + s.runBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + for _, bad := range []string{ + fmt.Sprintf("prctl;native;%s,99999", arg), + "setpriority;native;", + } { + s.runBpf(c, seccompWhitelist, bad, main.SeccompRetKill) + } + } + } + } +} + +// ported from test_restrictions_working_args_clone +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsClone(c *C) { + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input + {"setns - CLONE_NEWIPC", "setns;native;-,CLONE_NEWIPC", main.SeccompRetAllow}, + {"setns - CLONE_NEWNET", "setns;native;-,CLONE_NEWNET", main.SeccompRetAllow}, + {"setns - CLONE_NEWNS", "setns;native;-,CLONE_NEWNS", main.SeccompRetAllow}, + {"setns - CLONE_NEWPID", "setns;native;-,CLONE_NEWPID", main.SeccompRetAllow}, + {"setns - CLONE_NEWUSER", "setns;native;-,CLONE_NEWUSER", main.SeccompRetAllow}, + {"setns - CLONE_NEWUTS", "setns;native;-,CLONE_NEWUTS", main.SeccompRetAllow}, + // bad input + {"setns - CLONE_NEWIPC", "setns;native;-,99", main.SeccompRetKill}, + {"setns - CLONE_NEWNET", "setns;native;-,99", main.SeccompRetKill}, + {"setns - CLONE_NEWNS", "setns;native;-,99", main.SeccompRetKill}, + {"setns - CLONE_NEWPID", "setns;native;-,99", main.SeccompRetKill}, + {"setns - CLONE_NEWUSER", "setns;native;-,99", main.SeccompRetKill}, + {"setns - CLONE_NEWUTS", "setns;native;-,99", main.SeccompRetKill}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +// ported from test_restrictions_working_args_mknod +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsMknod(c *C) { + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input + {"mknod - S_IFREG", "mknod;native;-,S_IFREG", main.SeccompRetAllow}, + {"mknod - S_IFCHR", "mknod;native;-,S_IFCHR", main.SeccompRetAllow}, + {"mknod - S_IFBLK", "mknod;native;-,S_IFBLK", main.SeccompRetAllow}, + {"mknod - S_IFIFO", "mknod;native;-,S_IFIFO", main.SeccompRetAllow}, + {"mknod - S_IFSOCK", "mknod;native;-,S_IFSOCK", main.SeccompRetAllow}, + // bad input + {"mknod - S_IFREG", "mknod;native;-,999", main.SeccompRetKill}, + {"mknod - S_IFCHR", "mknod;native;-,999", main.SeccompRetKill}, + {"mknod - S_IFBLK", "mknod;native;-,999", main.SeccompRetKill}, + {"mknod - S_IFIFO", "mknod;native;-,999", main.SeccompRetKill}, + {"mknod - S_IFSOCK", "mknod;native;-,999", main.SeccompRetKill}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +// ported from test_restrictions_working_args_prio +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsPrio(c *C) { + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input + {"setpriority PRIO_PROCESS", "setpriority;native;PRIO_PROCESS", main.SeccompRetAllow}, + {"setpriority PRIO_PGRP", "setpriority;native;PRIO_PGRP", main.SeccompRetAllow}, + {"setpriority PRIO_USER", "setpriority;native;PRIO_USER", main.SeccompRetAllow}, + // bad input + {"setpriority PRIO_PROCESS", "setpriority;native;99", main.SeccompRetKill}, + {"setpriority PRIO_PGRP", "setpriority;native;99", main.SeccompRetKill}, + {"setpriority PRIO_USER", "setpriority;native;99", main.SeccompRetKill}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +// ported from test_restrictions_working_args_termios +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsTermios(c *C) { + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input + {"ioctl - TIOCSTI", "ioctl;native;-,TIOCSTI", main.SeccompRetAllow}, + // bad input + {"ioctl - TIOCSTI", "quotactl;native;-,99", main.SeccompRetKill}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsUidGid(c *C) { + // while 'root' user usually has uid 0, 'daemon' user uid may vary + // across distributions, best lookup the uid directly + daemonUid, err := osutil.FindUid("daemon") + c.Assert(err, IsNil) + + for _, t := range []struct { + seccompWhitelist string + bpfInput string + expected int + }{ + // good input. 'root' is guaranteed to be '0' and 'daemon' uid + // was determined at runtime + {"setuid u:root", "setuid;native;0", main.SeccompRetAllow}, + {"setuid u:daemon", fmt.Sprintf("setuid;native;%v", daemonUid), + main.SeccompRetAllow}, + {"setgid g:root", "setgid;native;0", main.SeccompRetAllow}, + {"setgid g:daemon", fmt.Sprintf("setgid;native;%v", daemonUid), + main.SeccompRetAllow}, + // bad input + {"setuid u:root", "setuid;native;99", main.SeccompRetKill}, + {"setuid u:daemon", "setuid;native;99", main.SeccompRetKill}, + {"setgid g:root", "setgid;native;99", main.SeccompRetKill}, + {"setgid g:daemon", "setgid;native;99", main.SeccompRetKill}, + } { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } +} + +func (s *snapSeccompSuite) TestCompatArchWorks(c *C) { + for _, t := range []struct { + arch string + seccompWhitelist string + bpfInput string + expected int + }{ + // on amd64 we add compat i386 + {"amd64", "read", "read;i386", main.SeccompRetAllow}, + {"amd64", "read", "read;amd64", main.SeccompRetAllow}, + } { + // It is tricky to mock the architecture here because + // seccomp is always adding the native arch to the seccomp + // filter and it will silently discard arches that have + // an endian mismatch: + // https://github.com/seccomp/libseccomp/issues/86 + // + // This means we can not just + // main.MockArchUbuntuArchitecture(t.arch) + // here because on endian mismatch the arch will *not* be + // added + if arch.UbuntuArchitecture() == t.arch { + s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected) + } + } +} diff --git a/cmd/snap-update-ns/bootstrap.c b/cmd/snap-update-ns/bootstrap.c new file mode 100644 index 00000000..5cf71f3a --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.c @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +// IMPORTANT: all the code in this file may be run with elevated privileges +// when invoking snap-update-ns from the setuid snap-confine. +// +// This file is a preprocessor for snap-update-ns' main() function. It will +// perform input validation and clear the environment so that snap-update-ns' +// go code runs with safe inputs when called by the setuid() snap-confine. + +#include "bootstrap.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// bootstrap_errno contains a copy of errno if a system call fails. +int bootstrap_errno = 0; +// bootstrap_msg contains a static string if something fails. +const char* bootstrap_msg = NULL; + +// setns_into_snap switches mount namespace into that of a given snap. +static int +setns_into_snap(const char* snap_name) +{ + // Construct the name of the .mnt file to open. + char buf[PATH_MAX] = { + 0, + }; + int n = snprintf(buf, sizeof buf, "/run/snapd/ns/%s.mnt", snap_name); + if (n >= sizeof buf || n < 0) { + bootstrap_errno = 0; + bootstrap_msg = "cannot format mount namespace file name"; + return -1; + } + + // Open the mount namespace file. + int fd = open(buf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot open mount namespace file"; + return -1; + } + + // Switch to the mount namespace of the given snap. + int err = setns(fd, CLONE_NEWNS); + if (err < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot switch mount namespace"; + }; + + close(fd); + return err; +} + +// TODO: reuse the code from snap-confine, if possible. +static int skip_lowercase_letters(const char** p) +{ + int skipped = 0; + const char* c; + for (c = *p; *c >= 'a' && *c <= 'z'; ++c) { + skipped += 1; + } + *p = (*p) + skipped; + return skipped; +} + +// TODO: reuse the code from snap-confine, if possible. +static int skip_digits(const char** p) +{ + int skipped = 0; + const char* c; + for (c = *p; *c >= '0' && *c <= '9'; ++c) { + skipped += 1; + } + *p = (*p) + skipped; + return skipped; +} + +// TODO: reuse the code from snap-confine, if possible. +static int skip_one_char(const char** p, char c) +{ + if (**p == c) { + *p += 1; + return 1; + } + return 0; +} + +// validate_snap_name performs full validation of the given name. +int validate_snap_name(const char* snap_name) +{ + // NOTE: This function should be synchronized with the two other + // implementations: sc_snap_name_validate and snap.ValidateName. + + // Ensure that name is not NULL + if (snap_name == NULL) { + bootstrap_msg = "snap name cannot be NULL"; + return -1; + } + // This is a regexp-free routine hand-codes the following pattern: + // + // "^([a-z0-9]+-?)*[a-z](-?[a-z0-9])*$" + // + // The only motivation for not using regular expressions is so that we + // don't run untrusted input against a potentially complex regular + // expression engine. + const char* p = snap_name; + if (skip_one_char(&p, '-')) { + bootstrap_msg = "snap name cannot start with a dash"; + return -1; + } + bool got_letter = false; + for (; *p != '\0';) { + if (skip_lowercase_letters(&p) > 0) { + got_letter = true; + continue; + } + if (skip_digits(&p) > 0) { + continue; + } + if (skip_one_char(&p, '-') > 0) { + if (*p == '\0') { + bootstrap_msg = "snap name cannot end with a dash"; + return -1; + } + if (skip_one_char(&p, '-') > 0) { + bootstrap_msg = "snap name cannot contain two consecutive dashes"; + return -1; + } + continue; + } + bootstrap_msg = "snap name must use lower case letters, digits or dashes"; + return -1; + } + if (!got_letter) { + bootstrap_msg = "snap name must contain at least one letter"; + return -1; + } + + bootstrap_msg = NULL; + return 0; +} + +// process_arguments parses given a command line +// argc and argv are defined as for the main() function +void process_arguments(int argc, char *const *argv, const char** snap_name_out, bool* should_setns_out) +{ + // Find the name of the called program. If it is ending with ".test" then do nothing. + // NOTE: This lets us use cgo/go to write tests without running the bulk + // of the code automatically. + // + if (argv == NULL || argc < 1) { + bootstrap_errno = 0; + bootstrap_msg = "argv0 is corrupted"; + return; + } + const char* argv0 = argv[0]; + const char* argv0_suffix_maybe = strstr(argv0, ".test"); + if (argv0_suffix_maybe != NULL && argv0_suffix_maybe[strlen(".test")] == '\0') { + bootstrap_errno = 0; + bootstrap_msg = "bootstrap is not enabled while testing"; + return; + } + + bool should_setns = true; + const char* snap_name = NULL; + + // Sanity check the command line arguments. The go parts will + // scan this too. + int i; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (arg[0] == '-') { + /* We have an option */ + if (!strcmp(arg, "--from-snap-confine")) { + // When we are running under "--from-snap-confine" + // option skip the setns call as snap-confine has + // already placed us in the right namespace. + should_setns = false; + } else { + bootstrap_errno = 0; + bootstrap_msg = "unsupported option"; + return; + } + } else { + // We expect a single positional argument: the snap name + if (snap_name != NULL) { + bootstrap_errno = 0; + bootstrap_msg = "too many positional arguments"; + return; + } + snap_name = arg; + } + } + + // If there's no snap name given, just bail out. + if (snap_name == NULL) { + bootstrap_errno = 0; + bootstrap_msg = "snap name not provided"; + return; + } + + // Ensure that the snap name is valid so that we don't blindly setns into + // something that is controlled by a potential attacker. + if (validate_snap_name(snap_name) < 0) { + bootstrap_errno = 0; + // bootstap_msg is set by validate_snap_name; + return; + } + // We have a valid snap name now so let's store it. + if (snap_name_out != NULL) { + *snap_name_out = snap_name; + } + if (should_setns_out != NULL) { + *should_setns_out = should_setns; + } + bootstrap_errno = 0; + bootstrap_msg = NULL; +} + +// bootstrap prepares snap-update-ns to work in the namespace of the snap given +// on command line. +void bootstrap(int argc, char **argv, char **envp) +{ + // We may have been started via a setuid-root snap-confine. In order to + // prevent environment-based attacks we start by erasing all environment + // variables. + char *snapd_debug = getenv("SNAPD_DEBUG"); + if (clearenv() != 0) { + bootstrap_errno = 0; + bootstrap_msg = "bootstrap could not clear the environment"; + return; + } + if (snapd_debug != NULL) { + setenv("SNAPD_DEBUG", snapd_debug, 0); + } + + // Analyze the read process cmdline to find the snap name and decide if we + // should use setns to jump into the mount namespace of a particular snap. + // This is spread out for easier testability. + const char* snap_name = NULL; + bool should_setns = false; + process_arguments(argc, argv, &snap_name, &should_setns); + if (snap_name != NULL && should_setns) { + setns_into_snap(snap_name); + // setns_into_snap sets bootstrap_{errno,msg} + } +} diff --git a/cmd/snap-update-ns/bootstrap.go b/cmd/snap-update-ns/bootstrap.go new file mode 100644 index 00000000..9a5eee9d --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.go @@ -0,0 +1,117 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +// Use a pre-main helper to switch the mount namespace. This is required as +// golang creates threads at will and setns(..., CLONE_NEWNS) fails if any +// threads apart from the main thread exist. + +/* + +#include +#include "bootstrap.h" + +// The bootstrap function is called by the loader before passing +// control to main. We are using `preinit_array` rather than +// `init_array` because the Go linker adds its own initialisation +// function to `init_array`, and having ours run second would defeat +// the purpose of the C bootstrap code. +// +// The `used` attribute ensures that the compiler doesn't oprimise out +// the variable on the mistaken belief that it isn't used. +__attribute__((section(".preinit_array"), used)) static typeof(&bootstrap) init = &bootstrap; + +// NOTE: do not add anything before the following `import "C"' +*/ +import "C" + +import ( + "errors" + "fmt" + "syscall" + "unsafe" +) + +var ( + // ErrNoNamespace is returned when a snap namespace does not exist. + ErrNoNamespace = errors.New("cannot update mount namespace that was not created yet") +) + +// IMPORTANT: all the code in this section may be run with elevated privileges +// when invoking snap-update-ns from the setuid snap-confine. + +// BootstrapError returns error (if any) encountered in pre-main C code. +func BootstrapError() error { + if C.bootstrap_msg == nil { + return nil + } + errno := syscall.Errno(C.bootstrap_errno) + // Translate EINVAL from setns or ENOENT from open into a dedicated error. + if errno == syscall.EINVAL || errno == syscall.ENOENT { + return ErrNoNamespace + } + if errno != 0 { + return fmt.Errorf("%s: %s", C.GoString(C.bootstrap_msg), errno) + } + return fmt.Errorf("%s", C.GoString(C.bootstrap_msg)) +} + +// END IMPORTANT + +func makeArgv(args []string) []*C.char { + // Create argv array with terminating NULL element + argv := make([]*C.char, len(args)+1) + for i, arg := range args { + argv[i] = C.CString(arg) + } + return argv +} + +func freeArgv(argv []*C.char) { + for _, arg := range argv { + C.free(unsafe.Pointer(arg)) + } +} + +// validateSnapName checks if snap name is valid. +// This also sets bootstrap_msg on failure. +func validateSnapName(snapName string) int { + cStr := C.CString(snapName) + defer C.free(unsafe.Pointer(cStr)) + return int(C.validate_snap_name(cStr)) +} + +// processArguments parses commnad line arguments. +// The argument cmdline is a string with embedded +// NUL bytes, separating particular arguments. +func processArguments(args []string) (snapName string, shouldSetNs bool) { + argv := makeArgv(args) + defer freeArgv(argv) + + var snapNameOut *C.char + var shouldSetNsOut C.bool + C.process_arguments(C.int(len(args)), &argv[0], &snapNameOut, &shouldSetNsOut) + if snapNameOut != nil { + snapName = C.GoString(snapNameOut) + } + shouldSetNs = bool(shouldSetNsOut) + + return snapName, shouldSetNs +} diff --git a/cmd/snap-update-ns/bootstrap.h b/cmd/snap-update-ns/bootstrap.h new file mode 100644 index 00000000..4ec342d3 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SNAPD_CMD_SNAP_UPDATE_NS_H +#define SNAPD_CMD_SNAP_UPDATE_NS_H + +#define _GNU_SOURCE + +#include +#include + +extern int bootstrap_errno; +extern const char* bootstrap_msg; + +void bootstrap(int argc, char **argv, char **envp); +void process_arguments(int argc, char *const *argv, const char** snap_name_out, bool* should_setns_out); +int validate_snap_name(const char* snap_name); + +#endif diff --git a/cmd/snap-update-ns/bootstrap_ppc64le.go b/cmd/snap-update-ns/bootstrap_ppc64le.go new file mode 100644 index 00000000..fdfc2543 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap_ppc64le.go @@ -0,0 +1,31 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +// +build ppc64le,go1.7,!go1.8 + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +/* +#cgo LDFLAGS: -no-pie + +// we need "-no-pie" for ppc64le,go1.7 to work around build failure on +// ppc64el with go1.7, see +// https://forum.snapcraft.io/t/snapd-master-fails-on-zesty-ppc64el-with-r-ppc64-addr16-ha-for-symbol-out-of-range/ +*/ +import "C" diff --git a/cmd/snap-update-ns/bootstrap_test.go b/cmd/snap-update-ns/bootstrap_test.go new file mode 100644 index 00000000..cafa51a1 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap_test.go @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" +) + +type bootstrapSuite struct{} + +var _ = Suite(&bootstrapSuite{}) + +// Check that ValidateSnapName rejects "/" and "..". +func (s *bootstrapSuite) TestValidateSnapName(c *C) { + c.Assert(update.ValidateSnapName("hello-world"), Equals, 0) + c.Assert(update.ValidateSnapName("hello/world"), Equals, -1) + c.Assert(update.ValidateSnapName("hello..world"), Equals, -1) + c.Assert(update.ValidateSnapName("INVALID"), Equals, -1) + c.Assert(update.ValidateSnapName("-invalid"), Equals, -1) + c.Assert(update.ValidateSnapName(""), Equals, -1) +} + +// Test various cases of command line handling. +func (s *bootstrapSuite) TestProcessArguments(c *C) { + cases := []struct { + cmdline []string + snapName string + shouldSetNs bool + errPattern string + }{ + // Corrupted buffer is dealt with. + {[]string{}, "", false, "argv0 is corrupted"}, + // When testing real bootstrap is identified and disabled. + {[]string{"argv0.test"}, "", false, "bootstrap is not enabled while testing"}, + // Snap name is mandatory. + {[]string{"argv0"}, "", false, "snap name not provided"}, + // Snap name is parsed correctly. + {[]string{"argv0", "snapname"}, "snapname", true, ""}, + // Onlye one snap name is allowed. + {[]string{"argv0", "snapone", "snaptwo"}, "", false, "too many positional arguments"}, + // Snap name is validated correctly. + {[]string{"argv0", ""}, "", false, "snap name must contain at least one letter"}, + {[]string{"argv0", "in--valid"}, "", false, "snap name cannot contain two consecutive dashes"}, + {[]string{"argv0", "invalid-"}, "", false, "snap name cannot end with a dash"}, + {[]string{"argv0", "@invalid"}, "", false, "snap name must use lower case letters, digits or dashes"}, + {[]string{"argv0", "INVALID"}, "", false, "snap name must use lower case letters, digits or dashes"}, + // The option --from-snap-confine disables setns. + {[]string{"argv0", "--from-snap-confine", "snapname"}, "snapname", false, ""}, + {[]string{"argv0", "snapname", "--from-snap-confine"}, "snapname", false, ""}, + // Unknown options are reported. + {[]string{"argv0", "-invalid"}, "", false, "unsupported option"}, + {[]string{"argv0", "--option"}, "", false, "unsupported option"}, + {[]string{"argv0", "--from-snap-confine", "-invalid", "snapname"}, "", false, "unsupported option"}, + } + for _, tc := range cases { + snapName, shouldSetNs := update.ProcessArguments(tc.cmdline) + err := update.BootstrapError() + comment := Commentf("failed with cmdline %q, expected error pattern %q, actual error %q", + tc.cmdline, tc.errPattern, err) + if tc.errPattern != "" { + c.Assert(err, ErrorMatches, tc.errPattern, comment) + } else { + c.Assert(err, IsNil, comment) + } + c.Check(snapName, Equals, tc.snapName) + c.Check(shouldSetNs, Equals, tc.shouldSetNs) + } +} diff --git a/cmd/snap-update-ns/change.go b/cmd/snap-update-ns/change.go new file mode 100644 index 00000000..d440f636 --- /dev/null +++ b/cmd/snap-update-ns/change.go @@ -0,0 +1,216 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "syscall" + + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/logger" +) + +// Action represents a mount action (mount, remount, unmount, etc). +type Action string + +const ( + // Keep indicates that a given mount entry should be kept as-is. + Keep Action = "keep" + // Mount represents an action that results in mounting something somewhere. + Mount Action = "mount" + // Unmount represents an action that results in unmounting something from somewhere. + Unmount Action = "unmount" + // Remount when needed +) + +// Change describes a change to the mount table (action and the entry to act on). +type Change struct { + Entry mount.Entry + Action Action +} + +// String formats mount change to a human-readable line. +func (c Change) String() string { + return fmt.Sprintf("%s (%s)", c.Action, c.Entry) +} + +// changePerform is Change.Perform that can be mocked for testing. +var changePerform = (*Change).Perform + +// Perform executes the desired mount or unmount change using system calls. +// Filesystems that depend on helper programs or multiple independent calls to +// the kernel (--make-shared, for example) are unsupported. +// +// Perform may synthesize *additional* changes that were necessary to perform +// this change (such as mounted tmpfs or overlayfs). +func (c *Change) Perform() ([]*Change, error) { + if c.Action == Mount { + mode := os.FileMode(0755) + uid := 0 + gid := 0 + // Create target mount directory if needed. + if err := ensureMountPoint(c.Entry.Dir, mode, uid, gid); err != nil { + return nil, err + } + // If this is a bind mount then create the source directory as well. + // This allows snaps to share a subset of their data easily. + flags, _ := mount.OptsToCommonFlags(c.Entry.Options) + if flags&syscall.MS_BIND != 0 { + if err := ensureMountPoint(c.Entry.Name, mode, uid, gid); err != nil { + return nil, err + } + } + } + return nil, c.lowLevelPerform() +} + +// lowLevelPerform is simple bridge from Change to mount / unmount syscall. +func (c *Change) lowLevelPerform() error { + switch c.Action { + case Mount: + flags, unparsed := mount.OptsToCommonFlags(c.Entry.Options) + err := sysMount(c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ",")) + logger.Debugf("mount %q %q %q %d %q -> %s", c.Entry.Name, c.Entry.Dir, c.Entry.Type, uintptr(flags), strings.Join(unparsed, ","), err) + return err + case Unmount: + err := sysUnmount(c.Entry.Dir, umountNoFollow) + logger.Debugf("umount %q -> %v", c.Entry.Dir, err) + return err + case Keep: + return nil + } + return fmt.Errorf("cannot process mount change, unknown action: %q", c.Action) +} + +// NeededChanges computes the changes required to change current to desired mount entries. +// +// The current and desired profiles is a fstab like list of mount entries. The +// lists are processed and a "diff" of mount changes is produced. The mount +// changes, when applied in order, transform the current profile into the +// desired profile. +func NeededChanges(currentProfile, desiredProfile *mount.Profile) []*Change { + // Copy both profiles as we will want to mutate them. + current := make([]mount.Entry, len(currentProfile.Entries)) + copy(current, currentProfile.Entries) + desired := make([]mount.Entry, len(desiredProfile.Entries)) + copy(desired, desiredProfile.Entries) + + // Clean the directory part of both profiles. This is done so that we can + // easily test if a given directory is a subdirectory with + // strings.HasPrefix coupled with an extra slash character. + for i := range current { + current[i].Dir = filepath.Clean(current[i].Dir) + } + for i := range desired { + desired[i].Dir = filepath.Clean(desired[i].Dir) + } + + // Sort both lists by directory name with implicit trailing slash. + sort.Sort(byMagicDir(current)) + sort.Sort(byMagicDir(desired)) + + // Construct a desired directory map. + desiredMap := make(map[string]*mount.Entry) + for i := range desired { + desiredMap[desired[i].Dir] = &desired[i] + } + + // Indexed by mount point path. + reuse := make(map[string]bool) + // Indexed by entry ID + desiredIDs := make(map[string]bool) + var skipDir string + + // Collect the IDs of desired changes. + // We need that below to keep implicit changes from the current profile. + for i := range desired { + desiredIDs[XSnapdEntryID(&desired[i])] = true + } + + // Compute reusable entries: those which are equal in current and desired and which + // are not prefixed by another entry that changed. + for i := range current { + dir := current[i].Dir + if skipDir != "" && strings.HasPrefix(dir, skipDir) { + logger.Debugf("skipping entry %q", current[i]) + continue + } + skipDir = "" // reset skip prefix as it no longer applies + + // Reuse synthetic entries if their needed-by entry is desired. + // Synthetic entries cannot exist on their own and always couple to a + // non-synthetic entry. + + // NOTE: Synthetic changes have a special purpose. + // + // They are a "shadow" of mount events that occurred to allow one of + // the desired mount entries to be possible. The changes have only one + // goal: tell snap-update-ns how those mount events can be undone in + // case they are no longer needed. The actual changes may have been + // different and may have involved steps not represented as synthetic + // mount entires as long as those synthetic entries can be undone to + // reverse the effect. In reality each non-tmpfs synthetic entry was + // constructed using a temporary bind mount that contained the original + // mount entries of a directory that was hidden with a tmpfs, but this + // fact was lost. + if XSnapdSynthetic(¤t[i]) && desiredIDs[XSnapdNeededBy(¤t[i])] { + logger.Debugf("reusing synthetic entry %q", current[i]) + reuse[dir] = true + continue + } + + // Reuse entries that are desired and identical in the current profile. + if entry, ok := desiredMap[dir]; ok && current[i].Equal(entry) { + logger.Debugf("reusing unchanged entry %q", current[i]) + reuse[dir] = true + continue + } + + skipDir = strings.TrimSuffix(dir, "/") + "/" + } + + logger.Debugf("desiredIDs: %v", desiredIDs) + logger.Debugf("reuse: %v", reuse) + + // We are now ready to compute the necessary mount changes. + var changes []*Change + + // Unmount entries not reused in reverse to handle children before their parent. + for i := len(current) - 1; i >= 0; i-- { + if reuse[current[i].Dir] { + changes = append(changes, &Change{Action: Keep, Entry: current[i]}) + } else { + changes = append(changes, &Change{Action: Unmount, Entry: current[i]}) + } + } + + // Mount desired entries not reused. + for i := range desired { + if !reuse[desired[i].Dir] { + changes = append(changes, &Change{Action: Mount, Entry: desired[i]}) + } + } + + return changes +} diff --git a/cmd/snap-update-ns/change_test.go b/cmd/snap-update-ns/change_test.go new file mode 100644 index 00000000..9d85ac95 --- /dev/null +++ b/cmd/snap-update-ns/change_test.go @@ -0,0 +1,488 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "errors" + "os" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/testutil" +) + +type changeSuite struct { + testutil.BaseTest + sys *update.SyscallRecorder +} + +var ( + errTesting = errors.New("testing") +) + +var _ = Suite(&changeSuite{}) + +func (s *changeSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + // Mock and record system interactions. + s.sys = &update.SyscallRecorder{} + s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) +} + +func (s *changeSuite) TestFakeFileInfo(c *C) { + c.Assert(update.FileInfoDir.IsDir(), Equals, true) + c.Assert(update.FileInfoFile.IsDir(), Equals, false) + c.Assert(update.FileInfoSymlink.IsDir(), Equals, false) +} + +func (s *changeSuite) TestString(c *C) { + change := update.Change{ + Entry: mount.Entry{Dir: "/a/b", Name: "/dev/sda1"}, + Action: update.Mount, + } + c.Assert(change.String(), Equals, "mount (/dev/sda1 /a/b none defaults 0 0)") +} + +// When there are no profiles we don't do anything. +func (s *changeSuite) TestNeededChangesNoProfiles(c *C) { + current := &mount.Profile{} + desired := &mount.Profile{} + changes := update.NeededChanges(current, desired) + c.Assert(changes, IsNil) +} + +// When the profiles are the same we don't do anything. +func (s *changeSuite) TestNeededChangesNoChange(c *C) { + current := &mount.Profile{Entries: []mount.Entry{{Dir: "/common/stuff"}}} + desired := &mount.Profile{Entries: []mount.Entry{{Dir: "/common/stuff"}}} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: mount.Entry{Dir: "/common/stuff"}, Action: update.Keep}, + }) +} + +// When the content interface is connected we should mount the new entry. +func (s *changeSuite) TestNeededChangesTrivialMount(c *C) { + current := &mount.Profile{} + desired := &mount.Profile{Entries: []mount.Entry{{Dir: "/common/stuff"}}} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: desired.Entries[0], Action: update.Mount}, + }) +} + +// When the content interface is disconnected we should unmount the mounted entry. +func (s *changeSuite) TestNeededChangesTrivialUnmount(c *C) { + current := &mount.Profile{Entries: []mount.Entry{{Dir: "/common/stuff"}}} + desired := &mount.Profile{} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: current.Entries[0], Action: update.Unmount}, + }) +} + +// When umounting we unmount children before parents. +func (s *changeSuite) TestNeededChangesUnmountOrder(c *C) { + current := &mount.Profile{Entries: []mount.Entry{ + {Dir: "/common/stuff/extra"}, + {Dir: "/common/stuff"}, + }} + desired := &mount.Profile{} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: mount.Entry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, + {Entry: mount.Entry{Dir: "/common/stuff"}, Action: update.Unmount}, + }) +} + +// When mounting we mount the parents before the children. +func (s *changeSuite) TestNeededChangesMountOrder(c *C) { + current := &mount.Profile{} + desired := &mount.Profile{Entries: []mount.Entry{ + {Dir: "/common/stuff/extra"}, + {Dir: "/common/stuff"}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: mount.Entry{Dir: "/common/stuff"}, Action: update.Mount}, + {Entry: mount.Entry{Dir: "/common/stuff/extra"}, Action: update.Mount}, + }) +} + +// When parent changes we don't reuse its children +func (s *changeSuite) TestNeededChangesChangedParentSameChild(c *C) { + current := &mount.Profile{Entries: []mount.Entry{ + {Dir: "/common/stuff", Name: "/dev/sda1"}, + {Dir: "/common/stuff/extra"}, + {Dir: "/common/unrelated"}, + }} + desired := &mount.Profile{Entries: []mount.Entry{ + {Dir: "/common/stuff", Name: "/dev/sda2"}, + {Dir: "/common/stuff/extra"}, + {Dir: "/common/unrelated"}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: mount.Entry{Dir: "/common/unrelated"}, Action: update.Keep}, + {Entry: mount.Entry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, + {Entry: mount.Entry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Unmount}, + {Entry: mount.Entry{Dir: "/common/stuff", Name: "/dev/sda2"}, Action: update.Mount}, + {Entry: mount.Entry{Dir: "/common/stuff/extra"}, Action: update.Mount}, + }) +} + +// When child changes we don't touch the unchanged parent +func (s *changeSuite) TestNeededChangesSameParentChangedChild(c *C) { + current := &mount.Profile{Entries: []mount.Entry{ + {Dir: "/common/stuff"}, + {Dir: "/common/stuff/extra", Name: "/dev/sda1"}, + {Dir: "/common/unrelated"}, + }} + desired := &mount.Profile{Entries: []mount.Entry{ + {Dir: "/common/stuff"}, + {Dir: "/common/stuff/extra", Name: "/dev/sda2"}, + {Dir: "/common/unrelated"}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: mount.Entry{Dir: "/common/unrelated"}, Action: update.Keep}, + {Entry: mount.Entry{Dir: "/common/stuff/extra", Name: "/dev/sda1"}, Action: update.Unmount}, + {Entry: mount.Entry{Dir: "/common/stuff"}, Action: update.Keep}, + {Entry: mount.Entry{Dir: "/common/stuff/extra", Name: "/dev/sda2"}, Action: update.Mount}, + }) +} + +// Unused bind mount farms are unmounted. +func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUnused(c *C) { + current := &mount.Profile{Entries: []mount.Entry{{ + // The tmpfs that lets us write into immutable squashfs. We mock + // x-snapd.needed-by to the last entry in the current profile (the bind + // mount). Mark it synthetic since it is a helper mount that is needed + // to facilitate the following mounts. + Name: "tmpfs", + Dir: "/snap/name/42/subdir", + Type: "tmpfs", + Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, + }, { + // A bind mount to preserve a directory hidden by the tmpfs (the mount + // point is created elsewhere). We mock x-snapd.needed-by to the + // location of the bind mount below that is no longer desired. + Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", + Dir: "/snap/name/42/subdir/existing", + Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, + }, { + // A bind mount to put some content from another snap. The bind mount + // is nothing special but the fact that it is possible is the reason + // the two entries above exist. The mount point (created) is created + // elsewhere. + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }}} + + desired := &mount.Profile{} + + changes := update.NeededChanges(current, desired) + + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: mount.Entry{ + Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", + Dir: "/snap/name/42/subdir/existing", + Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, + }, Action: update.Unmount}, + {Entry: mount.Entry{ + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }, Action: update.Unmount}, + {Entry: mount.Entry{ + Name: "tmpfs", + Dir: "/snap/name/42/subdir", + Type: "tmpfs", + Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, + }, Action: update.Unmount}, + }) +} + +func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUsed(c *C) { + // NOTE: the current profile is the same as in the test + // TestNeededChangesTmpfsBindMountFarmUnused written above. + current := &mount.Profile{Entries: []mount.Entry{{ + Name: "tmpfs", + Dir: "/snap/name/42/subdir", + Type: "tmpfs", + Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, + }, { + Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", + Dir: "/snap/name/42/subdir/existing", + Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, + }, { + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }}} + + desired := &mount.Profile{Entries: []mount.Entry{{ + // This is the only entry that we explicitly want but in order to + // support it we need to keep the remaining implicit entries. + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }}} + + changes := update.NeededChanges(current, desired) + + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: mount.Entry{ + Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", + Dir: "/snap/name/42/subdir/existing", + Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, + }, Action: update.Keep}, + {Entry: mount.Entry{ + Name: "/snap/other/123/libs", + Dir: "/snap/name/42/subdir/created", + Options: []string{"bind", "ro"}, + }, Action: update.Keep}, + {Entry: mount.Entry{ + Name: "tmpfs", + Dir: "/snap/name/42/subdir", + Type: "tmpfs", + Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, + }, Action: update.Keep}, + }) +} + +// cur = ['/a/b', '/a/b-1', '/a/b-1/3', '/a/b/c'] +// des = ['/a/b', '/a/b-1', '/a/b/c' +// +// We are smart about comparing entries as directories. Here even though "/a/b" +// is a prefix of "/a/b-1" it is correctly reused. +func (s *changeSuite) TestNeededChangesSmartEntryComparison(c *C) { + current := &mount.Profile{Entries: []mount.Entry{ + {Dir: "/a/b", Name: "/dev/sda1"}, + {Dir: "/a/b-1"}, + {Dir: "/a/b-1/3"}, + {Dir: "/a/b/c"}, + }} + desired := &mount.Profile{Entries: []mount.Entry{ + {Dir: "/a/b", Name: "/dev/sda2"}, + {Dir: "/a/b-1"}, + {Dir: "/a/b/c"}, + }} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: mount.Entry{Dir: "/a/b/c"}, Action: update.Unmount}, + {Entry: mount.Entry{Dir: "/a/b", Name: "/dev/sda1"}, Action: update.Unmount}, + {Entry: mount.Entry{Dir: "/a/b-1/3"}, Action: update.Unmount}, + {Entry: mount.Entry{Dir: "/a/b-1"}, Action: update.Keep}, + + {Entry: mount.Entry{Dir: "/a/b", Name: "/dev/sda2"}, Action: update.Mount}, + {Entry: mount.Entry{Dir: "/a/b/c"}, Action: update.Mount}, + }) +} + +// Change.Perform calls the mount system call. +func (s *changeSuite) TestPerformMount(c *C) { + s.sys.InsertLstatResult(`lstat "/target"`, update.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type"}} + synth, err := chg.Perform() + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + `mount "/source" "/target" "type" 0 ""`, + }) +} + +// Change.Perform calls the mount system call (for bind mounts). +func (s *changeSuite) TestPerformBindMount(c *C) { + s.sys.InsertLstatResult(`lstat "/source"`, update.FileInfoDir) + s.sys.InsertLstatResult(`lstat "/target"`, update.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type", Options: []string{"bind"}}} + synth, err := chg.Perform() + c.Check(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + `lstat "/source"`, + `mount "/source" "/target" "type" MS_BIND ""`, + }) +} + +// Change.Perform creates the missing mount target. +func (s *changeSuite) TestPerformMountAutomaticMkdirTarget(c *C) { + s.sys.InsertFault(`lstat "/target"`, os.ErrNotExist) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type"}} + synth, err := chg.Perform() + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, + `mkdirat 3 "target" 0755`, + `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, + `fchown 4 0 0`, + `close 4`, + `close 3`, + `mount "/source" "/target" "type" 0 ""`, + }) +} + +// Change.Perform creates the missing bind-mount source. +func (s *changeSuite) TestPerformMountAutomaticMkdirSource(c *C) { + s.sys.InsertLstatResult(`lstat "/target"`, update.FileInfoDir) + s.sys.InsertFault(`lstat "/source"`, os.ErrNotExist) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type", Options: []string{"bind"}}} + synth, err := chg.Perform() + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + `lstat "/source"`, + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, + `mkdirat 3 "source" 0755`, + `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, + `fchown 4 0 0`, + `close 4`, + `close 3`, + `mount "/source" "/target" "type" MS_BIND ""`, + }) +} + +// Change.Perform rejects mount target if it is a symlink. +func (s *changeSuite) TestPerformMountRejectsTargetSymlink(c *C) { + s.sys.InsertLstatResult(`lstat "/target"`, update.FileInfoSymlink) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type"}} + synth, err := chg.Perform() + c.Assert(err, ErrorMatches, `cannot use "/target" for mounting, not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + }) +} + +// Change.Perform rejects bind-mount target if it is a symlink. +func (s *changeSuite) TestPerformBindMountRejectsTargetSymlink(c *C) { + s.sys.InsertLstatResult(`lstat "/target"`, update.FileInfoSymlink) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type", Options: []string{"bind"}}} + synth, err := chg.Perform() + c.Assert(err, ErrorMatches, `cannot use "/target" for mounting, not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + }) +} + +// Change.Perform rejects bind-mount source if it is a symlink. +func (s *changeSuite) TestPerformBindMountRejectsSourceSymlink(c *C) { + s.sys.InsertLstatResult(`lstat "/target"`, update.FileInfoDir) + s.sys.InsertLstatResult(`lstat "/source"`, update.FileInfoSymlink) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type", Options: []string{"bind"}}} + synth, err := chg.Perform() + c.Assert(err, ErrorMatches, `cannot use "/source" for mounting, not a directory`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + `lstat "/source"`, + }) +} + +// Change.Perform returns errors from os.Lstat (apart from ErrNotExist) +func (s *changeSuite) TestPerformMountLstatError(c *C) { + s.sys.InsertFault(`lstat "/target"`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type"}} + synth, err := chg.Perform() + c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{`lstat "/target"`}) +} + +// Change.Perform returns errors from os.MkdirAll +func (s *changeSuite) TestPerformMountMkdirAllError(c *C) { + s.sys.InsertFault(`lstat "/target"`, os.ErrNotExist) + s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type"}} + synth, err := chg.Perform() + c.Assert(err, ErrorMatches, `cannot mkdir path segment "target": testing`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, + `mkdirat 3 "target" 0755`, + `close 3`, + }) +} + +// Change.Perform returns errors from mount system call +func (s *changeSuite) TestPerformMountError(c *C) { + s.sys.InsertLstatResult(`lstat "/target"`, update.FileInfoDir) + s.sys.InsertFault(`mount "/source" "/target" "type" 0 ""`, errTesting) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "/source", Dir: "/target", Type: "type"}} + synth, err := chg.Perform() + c.Assert(err, Equals, errTesting) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/target"`, + `mount "/source" "/target" "type" 0 ""`, + }) +} + +// Change.Perform passes unrecognized options to mount. +func (s *changeSuite) TestPerformMountOptions(c *C) { + s.sys.InsertLstatResult(`lstat "target"`, update.FileInfoDir) + chg := &update.Change{Action: update.Mount, Entry: mount.Entry{Name: "source", Dir: "target", Type: "type", Options: []string{"funky"}}} + synth, err := chg.Perform() + c.Assert(err, IsNil) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "target"`, + `mount "source" "target" "type" 0 "funky"`, + }) +} + +// Change.Perform calls the unmount system call. +func (s *changeSuite) TestPerformUnmount(c *C) { + chg := &update.Change{Action: update.Unmount, Entry: mount.Entry{Name: "source", Dir: "target", Type: "type"}} + synth, err := chg.Perform() + c.Assert(err, IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{`unmount "target" UMOUNT_NOFOLLOW`}) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform returns errors from unmount system call +func (s *changeSuite) TestPerformUnountError(c *C) { + s.sys.InsertFault(`unmount "target" UMOUNT_NOFOLLOW`, errTesting) + chg := &update.Change{Action: update.Unmount, Entry: mount.Entry{Name: "source", Dir: "target", Type: "type"}} + synth, err := chg.Perform() + c.Assert(err, Equals, errTesting) + c.Assert(s.sys.Calls(), DeepEquals, []string{`unmount "target" UMOUNT_NOFOLLOW`}) + c.Assert(synth, HasLen, 0) +} + +// Change.Perform handles unknown actions. +func (s *changeSuite) TestPerformUnknownAction(c *C) { + chg := &update.Change{Action: update.Action(42)} + synth, err := chg.Perform() + c.Assert(err, ErrorMatches, `cannot process mount change, unknown action: .*`) + c.Assert(synth, HasLen, 0) + c.Assert(s.sys.Calls(), HasLen, 0) +} diff --git a/cmd/snap-update-ns/entry.go b/cmd/snap-update-ns/entry.go new file mode 100644 index 00000000..6a38eb98 --- /dev/null +++ b/cmd/snap-update-ns/entry.go @@ -0,0 +1,129 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "math" + "os" + "regexp" + + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/osutil" +) + +var ( + validModeRe = regexp.MustCompile("^0[0-7]{3}$") + validUserGroupRe = regexp.MustCompile("(^[0-9]+$)|(^[a-z_][a-z0-9_-]*[$]?$)") +) + +// XSnapdMode returns the file mode associated with x-snapd.mode mount option. +// If the mode is not specified explicitly then a default mode of 0755 is assumed. +func XSnapdMode(e *mount.Entry) (os.FileMode, error) { + if opt, ok := e.OptStr("x-snapd.mode"); ok { + if !validModeRe.MatchString(opt) { + return 0, fmt.Errorf("cannot parse octal file mode from %q", opt) + } + var mode os.FileMode + n, err := fmt.Sscanf(opt, "%o", &mode) + if err != nil || n != 1 { + return 0, fmt.Errorf("cannot parse octal file mode from %q", opt) + } + return mode, nil + } + return 0755, nil +} + +// XSnapdUID returns the user associated with x-snapd-user mount option. If +// the mode is not specified explicitly then a default "root" use is +// returned. +func XSnapdUID(e *mount.Entry) (uid uint64, err error) { + if opt, ok := e.OptStr("x-snapd.uid"); ok { + if !validUserGroupRe.MatchString(opt) { + return math.MaxUint64, fmt.Errorf("cannot parse user name %q", opt) + } + // Try to parse a numeric ID first. + if n, err := fmt.Sscanf(opt, "%d", &uid); n == 1 && err == nil { + return uid, nil + } + // Fall-back to system name lookup. + if uid, err = osutil.FindUid(opt); err != nil { + // The error message from FindUid is not very useful so just skip it. + return math.MaxUint64, fmt.Errorf("cannot resolve user name %q", opt) + } + return uid, nil + } + return 0, nil +} + +// XSnapdGID returns the user associated with x-snapd-user mount option. If +// the mode is not specified explicitly then a default "root" use is +// returned. +func XSnapdGID(e *mount.Entry) (gid uint64, err error) { + if opt, ok := e.OptStr("x-snapd.gid"); ok { + if !validUserGroupRe.MatchString(opt) { + return math.MaxUint64, fmt.Errorf("cannot parse group name %q", opt) + } + // Try to parse a numeric ID first. + if n, err := fmt.Sscanf(opt, "%d", &gid); n == 1 && err == nil { + return gid, nil + } + // Fall-back to system name lookup. + if gid, err = osutil.FindGid(opt); err != nil { + // The error message from FindGid is not very useful so just skip it. + return math.MaxUint64, fmt.Errorf("cannot resolve group name %q", opt) + } + return gid, nil + } + return 0, nil +} + +// XSnapdEntryID returns the identifier of a given mount enrty. +// +// Identifiers are kept in the x-snapd.id mount option. The value is a string +// that identifies a mount entry and is stable across invocations of snapd. In +// absence of that identifier the entry mount point is returned. +func XSnapdEntryID(e *mount.Entry) string { + if val, ok := e.OptStr("x-snapd.id"); ok { + return val + } + return e.Dir +} + +// XSnapdNeededBy the identifier of an entry which needs this entry to function. +// +// The "needed by" identifiers are kept in the x-snapd.needed-by mount option. +// The value is a string that identifies another mount entry which, in order to +// be feasible, has spawned one or more additional support entries. Each such +// entry contains the needed-by attribute. +func XSnapdNeededBy(e *mount.Entry) string { + val, _ := e.OptStr("x-snapd.needed-by") + return val +} + +// XSnapdSynthetic returns true of a given mount entry is synthetic. +// +// Synthetic mount entries are created by snap-update-ns itself, separately +// from what snapd instructed. Such entries are needed to make other things +// possible. They are identified by having the "x-snapd.synthetic" mount +// option. +func XSnapdSynthetic(e *mount.Entry) bool { + return e.OptBool("x-snapd.synthetic") +} diff --git a/cmd/snap-update-ns/entry_test.go b/cmd/snap-update-ns/entry_test.go new file mode 100644 index 00000000..6c20fad6 --- /dev/null +++ b/cmd/snap-update-ns/entry_test.go @@ -0,0 +1,180 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "math" + "os" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" +) + +type entrySuite struct{} + +var _ = Suite(&entrySuite{}) + +func (s *entrySuite) TestXSnapdMode(c *C) { + // Mode has a default value. + e := &mount.Entry{} + mode, err := update.XSnapdMode(e) + c.Assert(err, IsNil) + c.Assert(mode, Equals, os.FileMode(0755)) + + // Mode is parsed from the x-snapd.mode= option. + e = &mount.Entry{Options: []string{"x-snapd.mode=0700"}} + mode, err = update.XSnapdMode(e) + c.Assert(err, IsNil) + c.Assert(mode, Equals, os.FileMode(0700)) + + // Empty value is invalid. + e = &mount.Entry{Options: []string{"x-snapd.mode="}} + _, err = update.XSnapdMode(e) + c.Assert(err, ErrorMatches, `cannot parse octal file mode from ""`) + + // As well as other bogus values. + e = &mount.Entry{Options: []string{"x-snapd.mode=pasta"}} + _, err = update.XSnapdMode(e) + c.Assert(err, ErrorMatches, `cannot parse octal file mode from "pasta"`) + + // And even valid values with trailing garbage. + e = &mount.Entry{Options: []string{"x-snapd.mode=0700pasta"}} + mode, err = update.XSnapdMode(e) + c.Assert(err, ErrorMatches, `cannot parse octal file mode from "0700pasta"`) + c.Assert(mode, Equals, os.FileMode(0)) +} + +func (s *entrySuite) TestXSnapdUID(c *C) { + // User has a default value. + e := &mount.Entry{} + uid, err := update.XSnapdUID(e) + c.Assert(err, IsNil) + c.Assert(uid, Equals, uint64(0)) + + // User is parsed from the x-snapd.uid= option. + nobodyUID, err := osutil.FindUid("nobody") + c.Assert(err, IsNil) + e = &mount.Entry{Options: []string{"x-snapd.uid=nobody"}} + uid, err = update.XSnapdUID(e) + c.Assert(err, IsNil) + c.Assert(uid, Equals, nobodyUID) + + // Numeric names are used as-is. + e = &mount.Entry{Options: []string{"x-snapd.uid=123"}} + uid, err = update.XSnapdUID(e) + c.Assert(err, IsNil) + c.Assert(uid, Equals, uint64(123)) + + // Unknown user names are invalid. + e = &mount.Entry{Options: []string{"x-snapd.uid=bogus"}} + uid, err = update.XSnapdUID(e) + c.Assert(err, ErrorMatches, `cannot resolve user name "bogus"`) + c.Assert(uid, Equals, uint64(math.MaxUint64)) + + // And even valid values with trailing garbage. + e = &mount.Entry{Options: []string{"x-snapd.uid=0bogus"}} + uid, err = update.XSnapdUID(e) + c.Assert(err, ErrorMatches, `cannot parse user name "0bogus"`) + c.Assert(uid, Equals, uint64(math.MaxUint64)) +} + +func (s *entrySuite) TestXSnapdGID(c *C) { + // Group has a default value. + e := &mount.Entry{} + gid, err := update.XSnapdGID(e) + c.Assert(err, IsNil) + c.Assert(gid, Equals, uint64(0)) + + // Group is parsed from the x-snapd-group= option. + var nogroup string + var nogroupGID uint64 + // try to cover differences between distributions and find a suitable + // 'nogroup' like group, eg. Ubuntu uses 'nogroup' while Arch uses + // 'nobody' + for _, grp := range []string{"nogroup", "nobody"} { + nogroup = grp + if gid, err := osutil.FindGid(grp); err == nil { + nogroup = grp + nogroupGID = gid + break + } + } + c.Assert([]string{"nogroup", "nobody"}, testutil.Contains, nogroup) + + e = &mount.Entry{ + Options: []string{fmt.Sprintf("x-snapd.gid=%s", nogroup)}, + } + gid, err = update.XSnapdGID(e) + c.Assert(err, IsNil) + c.Assert(gid, Equals, nogroupGID) + + // Numeric names are used as-is. + e = &mount.Entry{Options: []string{"x-snapd.gid=456"}} + gid, err = update.XSnapdGID(e) + c.Assert(err, IsNil) + c.Assert(gid, Equals, uint64(456)) + + // Unknown group names are invalid. + e = &mount.Entry{Options: []string{"x-snapd.gid=bogus"}} + gid, err = update.XSnapdGID(e) + c.Assert(err, ErrorMatches, `cannot resolve group name "bogus"`) + c.Assert(gid, Equals, uint64(math.MaxUint64)) + + // And even valid values with trailing garbage. + e = &mount.Entry{Options: []string{"x-snapd.gid=0bogus"}} + gid, err = update.XSnapdGID(e) + c.Assert(err, ErrorMatches, `cannot parse group name "0bogus"`) + c.Assert(gid, Equals, uint64(math.MaxUint64)) +} + +func (s *entrySuite) TestXSnapdEntryID(c *C) { + // Entry ID is optional and defaults to the mount point. + e := &mount.Entry{Dir: "/foo"} + c.Assert(update.XSnapdEntryID(e), Equals, "/foo") + + // Entry ID is parsed from the x-snapd.id= option. + e = &mount.Entry{Dir: "/foo", Options: []string{"x-snapd.id=foo"}} + c.Assert(update.XSnapdEntryID(e), Equals, "foo") +} + +func (s *entrySuite) TestXSnapdNeededBy(c *C) { + // The needed-by attribute is optional. + e := &mount.Entry{} + c.Assert(update.XSnapdNeededBy(e), Equals, "") + + // The needed-by attribute parsed from the x-snapd.needed-by= option. + e = &mount.Entry{Options: []string{"x-snap.id=foo", "x-snapd.needed-by=bar"}} + c.Assert(update.XSnapdNeededBy(e), Equals, "bar") +} + +func (s *entrySuite) TestXSnapdSynthetic(c *C) { + // Entries are not synthetic unless tagged as such. + e := &mount.Entry{} + c.Assert(update.XSnapdSynthetic(e), Equals, false) + + // Tagging is done with x-snapd.synthetic option. + e = &mount.Entry{Options: []string{"x-snapd.synthetic"}} + c.Assert(update.XSnapdSynthetic(e), Equals, true) +} diff --git a/cmd/snap-update-ns/export_test.go b/cmd/snap-update-ns/export_test.go new file mode 100644 index 00000000..fd6a9872 --- /dev/null +++ b/cmd/snap-update-ns/export_test.go @@ -0,0 +1,341 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "strings" + "syscall" + "time" + + . "gopkg.in/check.v1" +) + +var ( + // change + ValidateSnapName = validateSnapName + ProcessArguments = processArguments + // freezer + FreezeSnapProcesses = freezeSnapProcesses + ThawSnapProcesses = thawSnapProcesses + // utils + EnsureMountPoint = ensureMountPoint + PlanWritableMimic = planWritableMimic + SecureMkdirAll = secureMkdirAll + SecureMkfileAll = secureMkfileAll + SplitIntoSegments = splitIntoSegments + + // main + ComputeAndSaveChanges = computeAndSaveChanges +) + +// fakeFileInfo implements os.FileInfo for one of the tests. +// Most of the functions panic as we don't expect them to be called. +type fakeFileInfo struct { + name string + mode os.FileMode +} + +func (fi *fakeFileInfo) Name() string { return fi.name } +func (*fakeFileInfo) Size() int64 { panic("unexpected call") } +func (fi *fakeFileInfo) Mode() os.FileMode { return fi.mode } +func (*fakeFileInfo) ModTime() time.Time { panic("unexpected call") } +func (fi *fakeFileInfo) IsDir() bool { return fi.Mode().IsDir() } +func (*fakeFileInfo) Sys() interface{} { panic("unexpected call") } + +// Fake FileInfo objects for InsertLstatResult +var ( + FileInfoFile = &fakeFileInfo{} + FileInfoDir = &fakeFileInfo{mode: os.ModeDir} + FileInfoSymlink = &fakeFileInfo{mode: os.ModeSymlink} +) + +func FakeFileInfo(name string, mode os.FileMode) os.FileInfo { + return &fakeFileInfo{name: name, mode: mode} +} + +// Formatter for flags passed to open syscall. +func formatOpenFlags(flags int) string { + var fl []string + if flags&syscall.O_NOFOLLOW != 0 { + flags ^= syscall.O_NOFOLLOW + fl = append(fl, "O_NOFOLLOW") + } + if flags&syscall.O_CLOEXEC != 0 { + flags ^= syscall.O_CLOEXEC + fl = append(fl, "O_CLOEXEC") + } + if flags&syscall.O_DIRECTORY != 0 { + flags ^= syscall.O_DIRECTORY + fl = append(fl, "O_DIRECTORY") + } + if flags&syscall.O_RDWR != 0 { + flags ^= syscall.O_RDWR + fl = append(fl, "O_RDWR") + } + if flags&syscall.O_CREAT != 0 { + flags ^= syscall.O_CREAT + fl = append(fl, "O_CREAT") + } + if flags&syscall.O_EXCL != 0 { + flags ^= syscall.O_EXCL + fl = append(fl, "O_EXCL") + } + if flags != 0 { + panic(fmt.Errorf("unrecognized open flags %d", flags)) + } + if len(fl) == 0 { + return "0" + } + return strings.Join(fl, "|") +} + +// Formatter for flags passed to mount syscall. +func formatMountFlags(flags int) string { + var fl []string + if flags&syscall.MS_BIND == syscall.MS_BIND { + flags ^= syscall.MS_BIND + fl = append(fl, "MS_BIND") + } + if flags != 0 { + panic(fmt.Errorf("unrecognized mount flags %d", flags)) + } + if len(fl) == 0 { + return "0" + } + return strings.Join(fl, "|") +} + +// SystemCalls encapsulates various system interactions performed by this module. +type SystemCalls interface { + Lstat(name string) (os.FileInfo, error) + + Close(fd int) error + Fchown(fd int, uid int, gid int) error + Mkdirat(dirfd int, path string, mode uint32) error + Mount(source string, target string, fstype string, flags uintptr, data string) (err error) + Open(path string, flags int, mode uint32) (fd int, err error) + Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) + Unmount(target string, flags int) error +} + +// SyscallRecorder stores which system calls were invoked. +type SyscallRecorder struct { + calls []string + errors map[string]func() error + lstats map[string]*fakeFileInfo + fds map[int]string +} + +// InsertFault makes given subsequent call to return the specified error. +func (sys *SyscallRecorder) InsertFault(call string, err error) { + if sys.errors == nil { + sys.errors = make(map[string]func() error) + } + sys.errors[call] = func() error { return err } +} + +func (sys *SyscallRecorder) InsertFaultFunc(call string, fn func() error) { + if sys.errors == nil { + sys.errors = make(map[string]func() error) + } + sys.errors[call] = fn +} + +// InsertLstatResult makes given subsequent call lstat return the specified fake file info. +func (sys *SyscallRecorder) InsertLstatResult(call string, fi *fakeFileInfo) { + if sys.lstats == nil { + sys.lstats = make(map[string]*fakeFileInfo) + } + sys.lstats[call] = fi +} + +// Calls returns the sequence of mocked calls that have been made. +func (sys *SyscallRecorder) Calls() []string { + return sys.calls +} + +// call remembers that a given call has occurred and returns a pre-arranged error, if any +func (sys *SyscallRecorder) call(call string) error { + sys.calls = append(sys.calls, call) + if fn := sys.errors[call]; fn != nil { + return fn() + } + return nil +} + +// allocFd assigns a file descriptor to a given operation. +func (sys *SyscallRecorder) allocFd(name string) int { + if sys.fds == nil { + sys.fds = make(map[int]string) + } + + // Use 3 as the lowest number for tests to look more plausible. + for i := 3; i < 100; i++ { + if _, ok := sys.fds[i]; !ok { + sys.fds[i] = name + return i + } + } + panic("cannot find unused file descriptor") +} + +// freeFd closes an open file descriptor. +func (sys *SyscallRecorder) freeFd(fd int) error { + if _, ok := sys.fds[fd]; !ok { + return fmt.Errorf("attempting to close closed file descriptor %d", fd) + } + delete(sys.fds, fd) + return nil +} + +func (sys *SyscallRecorder) CheckForStrayDescriptors(c *C) { + for fd, ok := range sys.fds { + c.Assert(ok, Equals, false, Commentf("unclosed file descriptor %d", fd)) + } +} + +func (sys *SyscallRecorder) Close(fd int) error { + if err := sys.call(fmt.Sprintf("close %d", fd)); err != nil { + return err + } + return sys.freeFd(fd) +} + +func (sys *SyscallRecorder) Fchown(fd int, uid int, gid int) error { + return sys.call(fmt.Sprintf("fchown %d %d %d", fd, uid, gid)) +} + +func (sys *SyscallRecorder) Mkdirat(dirfd int, path string, mode uint32) error { + return sys.call(fmt.Sprintf("mkdirat %d %q %#o", dirfd, path, mode)) +} + +func (sys *SyscallRecorder) Open(path string, flags int, mode uint32) (int, error) { + call := fmt.Sprintf("open %q %s %#o", path, formatOpenFlags(flags), mode) + if err := sys.call(call); err != nil { + return -1, err + } + return sys.allocFd(call), nil +} + +func (sys *SyscallRecorder) Openat(dirfd int, path string, flags int, mode uint32) (int, error) { + call := fmt.Sprintf("openat %d %q %s %#o", dirfd, path, formatOpenFlags(flags), mode) + if err := sys.call(call); err != nil { + return -1, err + } + return sys.allocFd(call), nil +} + +func (sys *SyscallRecorder) Mount(source string, target string, fstype string, flags uintptr, data string) (err error) { + return sys.call(fmt.Sprintf("mount %q %q %q %s %q", source, target, fstype, formatMountFlags(int(flags)), data)) +} + +func (sys *SyscallRecorder) Unmount(target string, flags int) (err error) { + if flags == umountNoFollow { + return sys.call(fmt.Sprintf("unmount %q %s", target, "UMOUNT_NOFOLLOW")) + } + return sys.call(fmt.Sprintf("unmount %q %d", target, flags)) +} + +func (sys *SyscallRecorder) Lstat(name string) (os.FileInfo, error) { + call := fmt.Sprintf("lstat %q", name) + if err := sys.call(call); err != nil { + return nil, err + } + if fi := sys.lstats[call]; fi != nil { + return fi, nil + } + panic(fmt.Sprintf("one of InsertLstatResult() or InsertFault() for %q must be used", call)) +} + +// MockSystemCalls replaces real system calls with those of the argument. +func MockSystemCalls(sc SystemCalls) (restore func()) { + //save + oldOsLstat := osLstat + + oldSysClose := sysClose + oldSysFchown := sysFchown + oldSysMkdirat := sysMkdirat + oldSysMount := sysMount + oldSysOpen := sysOpen + oldSysOpenat := sysOpenat + oldSysUnmount := sysUnmount + + // override + osLstat = sc.Lstat + + sysClose = sc.Close + sysFchown = sc.Fchown + sysMkdirat = sc.Mkdirat + sysMount = sc.Mount + sysOpen = sc.Open + sysOpenat = sc.Openat + sysUnmount = sc.Unmount + + return func() { + // restore + osLstat = oldOsLstat + + sysClose = oldSysClose + sysFchown = oldSysFchown + sysMkdirat = oldSysMkdirat + sysMount = oldSysMount + sysOpen = oldSysOpen + sysOpenat = oldSysOpenat + sysUnmount = oldSysUnmount + } +} + +func MockFreezerCgroupDir(c *C) (restore func()) { + old := freezerCgroupDir + freezerCgroupDir = c.MkDir() + return func() { + freezerCgroupDir = old + } +} + +func FreezerCgroupDir() string { + return freezerCgroupDir +} + +func MockChangePerform(f func(chg *Change) ([]*Change, error)) func() { + origChangePerform := changePerform + changePerform = f + return func() { + changePerform = origChangePerform + } +} + +func MockReadDir(fn func(string) ([]os.FileInfo, error)) (restore func()) { + old := ioutilReadDir + ioutilReadDir = fn + return func() { + ioutilReadDir = old + } +} + +func MockReadlink(fn func(string) (string, error)) (restore func()) { + old := osReadlink + osReadlink = fn + return func() { + osReadlink = old + } +} diff --git a/cmd/snap-update-ns/freezer.go b/cmd/snap-update-ns/freezer.go new file mode 100644 index 00000000..3d09d5dd --- /dev/null +++ b/cmd/snap-update-ns/freezer.go @@ -0,0 +1,74 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" +) + +var freezerCgroupDir = "/sys/fs/cgroup/freezer" + +// freezeSnapProcesses freezes all the processes originating from the given snap. +// Processes are frozen regardless of which particular snap application they +// originate from. +func freezeSnapProcesses(snapName string) error { + fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state") + if err := ioutil.WriteFile(fname, []byte("FROZEN"), 0644); err != nil && os.IsNotExist(err) { + // When there's no freezer cgroup we don't have to freeze anything. + // This can happen when no process belonging to a given snap has been + // started yet. + return nil + } else if err != nil { + return fmt.Errorf("cannot freeze processes of snap %q, %v", snapName, err) + } + for i := 0; i < 30; i++ { + data, err := ioutil.ReadFile(fname) + if err != nil { + return fmt.Errorf("cannot determine the freeze state of processes of snap %q, %v", snapName, err) + } + // If the cgroup is still freezing then wait a moment and try again. + if bytes.Equal(data, []byte("FREEZING")) { + time.Sleep(100 * time.Millisecond) + continue + } + return nil + } + // If we got here then we timed out after seeing FREEZING for too long. + thawSnapProcesses(snapName) // ignore the error, this is best-effort. + return fmt.Errorf("cannot finish freezing processes of snap %q", snapName) +} + +func thawSnapProcesses(snapName string) error { + fname := filepath.Join(freezerCgroupDir, fmt.Sprintf("snap.%s", snapName), "freezer.state") + if err := ioutil.WriteFile(fname, []byte("THAWED"), 0644); err != nil && os.IsNotExist(err) { + // When there's no freezer cgroup we don't have to thaw anything. + // This can happen when no process belonging to a given snap has been + // started yet. + return nil + } else if err != nil { + return fmt.Errorf("cannot thaw processes of snap %q", snapName) + } + return nil +} diff --git a/cmd/snap-update-ns/freezer_test.go b/cmd/snap-update-ns/freezer_test.go new file mode 100644 index 00000000..a0074440 --- /dev/null +++ b/cmd/snap-update-ns/freezer_test.go @@ -0,0 +1,95 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" +) + +type freezerSuite struct{} + +var _ = Suite(&freezerSuite{}) + +func (s *freezerSuite) TestFreezeSnapProcesses(c *C) { + restore := update.MockFreezerCgroupDir(c) + defer restore() + + n := "foo" // snap name + p := filepath.Join(update.FreezerCgroupDir(), fmt.Sprintf("snap.%s", n)) // snap freezer cgroup + f := filepath.Join(p, "freezer.state") // freezer.state file of the cgroup + + // When the freezer cgroup filesystem doesn't exist we do nothing at all. + c.Assert(update.FreezeSnapProcesses(n), IsNil) + _, err := os.Stat(f) + c.Assert(os.IsNotExist(err), Equals, true) + + // When the freezer cgroup filesystem exists but the particular cgroup + // doesn't exist we don nothing at all. + c.Assert(os.MkdirAll(update.FreezerCgroupDir(), 0755), IsNil) + c.Assert(update.FreezeSnapProcesses(n), IsNil) + _, err = os.Stat(f) + c.Assert(os.IsNotExist(err), Equals, true) + + // When the cgroup exists we write FROZEN the freezer.state file. + c.Assert(os.MkdirAll(p, 0755), IsNil) + c.Assert(update.FreezeSnapProcesses(n), IsNil) + _, err = os.Stat(f) + c.Assert(err, IsNil) + data, err := ioutil.ReadFile(f) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte(`FROZEN`)) +} + +func (s *freezerSuite) TestThawSnapProcesses(c *C) { + restore := update.MockFreezerCgroupDir(c) + defer restore() + + n := "foo" // snap name + p := filepath.Join(update.FreezerCgroupDir(), fmt.Sprintf("snap.%s", n)) // snap freezer cgroup + f := filepath.Join(p, "freezer.state") // freezer.state file of the cgroup + + // When the freezer cgroup filesystem doesn't exist we do nothing at all. + c.Assert(update.ThawSnapProcesses(n), IsNil) + _, err := os.Stat(f) + c.Assert(os.IsNotExist(err), Equals, true) + + // When the freezer cgroup filesystem exists but the particular cgroup + // doesn't exist we don nothing at all. + c.Assert(os.MkdirAll(update.FreezerCgroupDir(), 0755), IsNil) + c.Assert(update.ThawSnapProcesses(n), IsNil) + _, err = os.Stat(f) + c.Assert(os.IsNotExist(err), Equals, true) + + // When the cgroup exists we write THAWED the freezer.state file. + c.Assert(os.MkdirAll(p, 0755), IsNil) + c.Assert(update.ThawSnapProcesses(n), IsNil) + _, err = os.Stat(f) + c.Assert(err, IsNil) + data, err := ioutil.ReadFile(f) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte(`THAWED`)) +} diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go new file mode 100644 index 00000000..33231630 --- /dev/null +++ b/cmd/snap-update-ns/main.go @@ -0,0 +1,208 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" +) + +var opts struct { + FromSnapConfine bool `long:"from-snap-confine"` + Positionals struct { + SnapName string `positional-arg-name:"SNAP_NAME" required:"yes"` + } `positional-args:"true"` +} + +// IMPORTANT: all the code in main() until bootstrap is finished may be run +// with elevated privileges when invoking snap-update-ns from the setuid +// snap-confine. + +func main() { + logger.SimpleSetup() + if err := run(); err != nil { + fmt.Printf("cannot update snap namespace: %s\n", err) + os.Exit(1) + } + // END IMPORTANT +} + +func parseArgs(args []string) error { + parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) + _, err := parser.ParseArgs(args) + return err +} + +// IMPORTANT: all the code in run() until BootStrapError() is finished may +// be run with elevated privileges when invoking snap-update-ns from +// the setuid snap-confine. + +func run() error { + // There is some C code that runs before main() is started. + // That code always runs and sets an error condition if it fails. + // Here we just check for the error. + if err := BootstrapError(); err != nil { + // If there is no mount namespace to transition to let's just quit + // instantly without any errors as there is nothing to do anymore. + if err == ErrNoNamespace { + logger.Debugf("no preserved mount namespace, nothing to update") + return nil + } + return err + } + // END IMPORTANT + + if err := parseArgs(os.Args[1:]); err != nil { + return err + } + + snapName := opts.Positionals.SnapName + + // Lock the mount namespace so that any concurrently attempted invocations + // of snap-confine are synchronized and will see consistent state. + lock, err := mount.OpenLock(snapName) + if err != nil { + return fmt.Errorf("cannot open lock file for mount namespace of snap %q: %s", snapName, err) + } + defer func() { + logger.Debugf("unlocking mount namespace of snap %q", snapName) + lock.Close() + }() + + logger.Debugf("locking mount namespace of snap %q", snapName) + if opts.FromSnapConfine { + // When --from-snap-confine is passed then we just ensure that the + // namespace is locked. This is used by snap-confine to use + // snap-update-ns to apply mount profiles. + if err := lock.TryLock(); err != osutil.ErrAlreadyLocked { + return fmt.Errorf("mount namespace of snap %q is not locked but --from-snap-confine was used", snapName) + } + } else { + if err := lock.Lock(); err != nil { + return fmt.Errorf("cannot lock mount namespace of snap %q: %s", snapName, err) + } + } + + // Freeze the mount namespace and unfreeze it later. This lets us perform + // modifications without snap processes attempting to construct + // symlinks or perform other malicious activity (such as attempting to + // introduce a symlink that would cause us to mount something other + // than what we expected). + logger.Debugf("freezing processes of snap %q", snapName) + if err := freezeSnapProcesses(opts.Positionals.SnapName); err != nil { + return err + } + defer func() { + logger.Debugf("thawing processes of snap %q", snapName) + thawSnapProcesses(opts.Positionals.SnapName) + }() + + return computeAndSaveChanges(snapName) +} + +func computeAndSaveChanges(snapName string) error { + // Read the desired and current mount profiles. Note that missing files + // count as empty profiles so that we can gracefully handle a mount + // interface connection/disconnection. + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + desired, err := mount.LoadProfile(desiredProfilePath) + if err != nil { + return fmt.Errorf("cannot load desired mount profile of snap %q: %s", snapName, err) + } + debugShowProfile(desired, "desired mount profile") + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + currentBefore, err := mount.LoadProfile(currentProfilePath) + if err != nil { + return fmt.Errorf("cannot load current mount profile of snap %q: %s", snapName, err) + } + debugShowProfile(currentBefore, "current mount profile (before applying changes)") + + // Compute the needed changes and perform each change if needed, collecting + // those that we managed to perform or that were performed already. + changesNeeded := NeededChanges(currentBefore, desired) + debugShowChanges(changesNeeded, "mount changes needed") + + logger.Debugf("performing mount changes:") + var changesMade []*Change + for _, change := range changesNeeded { + logger.Debugf("\t * %s", change) + synthesised, err := changePerform(change) + // NOTE: we may have done something even if Perform itself has failed. + // We need to collect synthesized changes and store them. + changesMade = append(changesMade, synthesised...) + if len(synthesised) > 0 { + logger.Debugf("\tsynthesised additional mount changes:") + for _, synth := range synthesised { + logger.Debugf(" * \t\t%s", synth) + } + } + if err != nil { + logger.Noticef("cannot change mount namespace of snap %q according to change %s: %s", snapName, change, err) + continue + } + changesMade = append(changesMade, change) + } + + // Compute the new current profile so that it contains only changes that were made + // and save it back for next runs. + var currentAfter mount.Profile + for _, change := range changesMade { + if change.Action == Mount || change.Action == Keep { + currentAfter.Entries = append(currentAfter.Entries, change.Entry) + } + } + debugShowProfile(¤tAfter, "current mount profile (after applying changes)") + + logger.Debugf("saving current mount profile of snap %q", snapName) + if err := currentAfter.Save(currentProfilePath); err != nil { + return fmt.Errorf("cannot save current mount profile of snap %q: %s", snapName, err) + } + return nil +} + +func debugShowProfile(profile *mount.Profile, header string) { + if len(profile.Entries) > 0 { + logger.Debugf("%s:", header) + for _, entry := range profile.Entries { + logger.Debugf("\t%s", entry) + } + } else { + logger.Debugf("%s: (none)", header) + } +} + +func debugShowChanges(changes []*Change, header string) { + if len(changes) > 0 { + logger.Debugf("%s:", header) + for _, change := range changes { + logger.Debugf("\t%s", change) + } + } else { + logger.Debugf("%s: (none)", header) + } +} diff --git a/cmd/snap-update-ns/main_test.go b/cmd/snap-update-ns/main_test.go new file mode 100644 index 00000000..47e29562 --- /dev/null +++ b/cmd/snap-update-ns/main_test.go @@ -0,0 +1,220 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces/mount" +) + +func Test(t *testing.T) { TestingT(t) } + +type mainSuite struct{} + +var _ = Suite(&mainSuite{}) + +func (s *mainSuite) TestComputeAndSaveChanges(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + restore := update.MockChangePerform(func(chg *update.Change) ([]*update.Change, error) { + return nil, nil + }) + defer restore() + + snapName := "foo" + desiredProfileContent := `/var/lib/snapd/hostfs/usr/share/fonts /usr/share/fonts none bind,ro 0 0 +/var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0` + + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + err := os.MkdirAll(filepath.Dir(desiredProfilePath), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644) + c.Assert(err, IsNil) + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + err = os.MkdirAll(filepath.Dir(currentProfilePath), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(currentProfilePath, nil, 0644) + c.Assert(err, IsNil) + + err = update.ComputeAndSaveChanges(snapName) + c.Assert(err, IsNil) + + content, err := ioutil.ReadFile(currentProfilePath) + c.Assert(err, IsNil) + c.Check(string(content), Equals, `/var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0 +/var/lib/snapd/hostfs/usr/share/fonts /usr/share/fonts none bind,ro 0 0 +`) +} + +func (s *mainSuite) TestAddingSyntheticChanges(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + // The snap `mysnap` wishes to export it's usr/share/mysnap directory and + // make it appear as if it was in /usr/share/mysnap directly. + const snapName = "mysnap" + const currentProfileContent = "" + const desiredProfileContent = "/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0" + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + + c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) + c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) + c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) + + // In order to make that work, /usr/share had to be converted to a writable + // mimic. Some actions were performed under the hood and now we see a + // subset of them as synthetic changes here. + // + // Note that if you compare this to the code that plans a writable mimic + // you will see that there are additional changes that are _not_ + // represented here. The changes have only one goal: tell + // snap-update-ns how the mimic can be undone in case it is no longer + // needed. + restore := update.MockChangePerform(func(chg *update.Change) ([]*update.Change, error) { + // The change that we were asked to perform is to create a bind mount + // from within the snap to /usr/share/mysnap. + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Mount, Entry: mount.Entry{ + Name: "/snap/mysnap/42/usr/share/mysnap", + Dir: "/usr/share/mysnap", Type: "none", + Options: []string{"bind", "ro"}}}) + synthetic := []*update.Change{ + // The original directory (which was a part of the core snap and is + // read only) was hidden with a tmpfs. + {Action: update.Mount, Entry: mount.Entry{ + Dir: "/usr/share", Name: "tmpfs", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, + // For the sake of brevity we will only represent a few of the + // entries typically there. Normally this list can get quite long. + // Also note that the entry is a little fake. In reality it was + // constructed using a temporary bind mount that contained the + // original mount entries of /usr/share but this fact was lost. + // Again, the only point of this entry is to correctly perform an + // undo operation when /usr/share/mysnap is no longer needed. + {Action: update.Mount, Entry: mount.Entry{ + Dir: "/usr/share/adduser", Name: "/usr/share/adduser", + Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, + {Action: update.Mount, Entry: mount.Entry{ + Dir: "/usr/share/awk", Name: "/usr/share/awk", + Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, + } + return synthetic, nil + }) + defer restore() + + c.Assert(update.ComputeAndSaveChanges(snapName), IsNil) + + content, err := ioutil.ReadFile(currentProfilePath) + c.Assert(err, IsNil) + c.Check(string(content), Equals, + `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/usr/share/adduser /usr/share/adduser none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/usr/share/awk /usr/share/awk none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0 +`) +} + +func (s *mainSuite) TestRemovingSyntheticChanges(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + // The snap `mysnap` no longer wishes to export it's usr/share/mysnap + // directory. All the synthetic changes that were associated with that mount + // entry can be discarded. + const snapName = "mysnap" + const currentProfileContent = `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/usr/share/adduser /usr/share/adduser none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/usr/share/awk /usr/share/awk none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 +/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0 +` + const desiredProfileContent = "" + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + + c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) + c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) + c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) + + n := -1 + restore := update.MockChangePerform(func(chg *update.Change) ([]*update.Change, error) { + n += 1 + switch n { + case 0: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Unmount, + Entry: mount.Entry{ + Name: "/snap/mysnap/42/usr/share/mysnap", + Dir: "/usr/share/mysnap", Type: "none", + Options: []string{"bind", "ro"}, + }, + }) + case 1: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Unmount, + Entry: mount.Entry{ + Name: "/usr/share/awk", Dir: "/usr/share/awk", Type: "none", + Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, + }, + }) + case 2: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Unmount, + Entry: mount.Entry{ + Name: "/usr/share/adduser", Dir: "/usr/share/adduser", Type: "none", + Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, + }, + }) + case 3: + c.Assert(chg, DeepEquals, &update.Change{ + Action: update.Unmount, + Entry: mount.Entry{ + Name: "tmpfs", Dir: "/usr/share", Type: "tmpfs", + Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}, + }, + }) + default: + panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) + } + return nil, nil + }) + defer restore() + + c.Assert(update.ComputeAndSaveChanges(snapName), IsNil) + + content, err := ioutil.ReadFile(currentProfilePath) + c.Assert(err, IsNil) + c.Check(string(content), Equals, "") +} diff --git a/cmd/snap-update-ns/sorting.go b/cmd/snap-update-ns/sorting.go new file mode 100644 index 00000000..09146c4e --- /dev/null +++ b/cmd/snap-update-ns/sorting.go @@ -0,0 +1,44 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "strings" + + "github.com/snapcore/snapd/interfaces/mount" +) + +// byMagicDir allows sorting an array of entries that automagically assumes +// each entry ends with a trailing slash. +type byMagicDir []mount.Entry + +func (c byMagicDir) Len() int { return len(c) } +func (c byMagicDir) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c byMagicDir) Less(i, j int) bool { + iDir := c[i].Dir + jDir := c[j].Dir + if !strings.HasSuffix(iDir, "/") { + iDir = iDir + "/" + } + if !strings.HasSuffix(jDir, "/") { + jDir = jDir + "/" + } + return iDir < jDir +} diff --git a/cmd/snap-update-ns/sorting_test.go b/cmd/snap-update-ns/sorting_test.go new file mode 100644 index 00000000..36edda7d --- /dev/null +++ b/cmd/snap-update-ns/sorting_test.go @@ -0,0 +1,50 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "sort" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces/mount" +) + +type sortSuite struct{} + +var _ = Suite(&sortSuite{}) + +func (s *sortSuite) TestTrailingSlashesComparison(c *C) { + // Naively sorted entries. + entries := []mount.Entry{ + {Dir: "/a/b"}, + {Dir: "/a/b-1"}, + {Dir: "/a/b-1/3"}, + {Dir: "/a/b/c"}, + } + sort.Sort(byMagicDir(entries)) + // Entries sorted as if they had a trailing slash. + c.Assert(entries, DeepEquals, []mount.Entry{ + {Dir: "/a/b-1"}, + {Dir: "/a/b-1/3"}, + {Dir: "/a/b"}, + {Dir: "/a/b/c"}, + }) +} diff --git a/cmd/snap-update-ns/utils.go b/cmd/snap-update-ns/utils.go new file mode 100644 index 00000000..39bbe16f --- /dev/null +++ b/cmd/snap-update-ns/utils.go @@ -0,0 +1,387 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/logger" +) + +// not available through syscall +const ( + umountNoFollow = 8 +) + +// For mocking everything during testing. +var ( + osLstat = os.Lstat + osReadlink = os.Readlink + + sysClose = syscall.Close + sysMkdirat = syscall.Mkdirat + sysMount = syscall.Mount + sysOpen = syscall.Open + sysOpenat = syscall.Openat + sysUnmount = syscall.Unmount + sysFchown = syscall.Fchown + + ioutilReadDir = ioutil.ReadDir +) + +// ReadOnlyFsError is an error encapsulating encountered EROFS. +type ReadOnlyFsError struct { + Path string +} + +func (e *ReadOnlyFsError) Error() string { + return fmt.Sprintf("cannot operate on read-only filesystem at %s", e.Path) +} + +// Create directories for all but the last segments and return the file +// descriptor to the leaf directory. This function is a base for secure +// variants of mkdir, touch and symlink. +func secureMkPrefix(segments []string, perm os.FileMode, uid, gid int) (int, error) { + logger.Debugf("secure-mk-prefix %q %v %d %d -> ...", segments, perm, uid, gid) + + // Declare var and don't assign-declare below to ensure we don't swallow + // any errors by mistake. + var err error + var fd int + + const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY + + // Open the root directory and start there. + fd, err = sysOpen("/", openFlags, 0) + if err != nil { + return -1, fmt.Errorf("cannot open root directory: %v", err) + } + if len(segments) > 1 { + defer sysClose(fd) + } + + if len(segments) > 0 { + // Process all but the last segment. + for i := range segments[:len(segments)-1] { + fd, err = secureMkDir(fd, segments, i, perm, uid, gid) + if err != nil { + return -1, err + } + // Keep the final FD open (caller needs to close it). + if i < len(segments)-2 { + defer sysClose(fd) + } + } + } + + logger.Debugf("secure-mk-prefix %q %v %d %d -> %d", segments, perm, uid, gid, fd) + return fd, nil +} + +// secureMkDir creates a directory at i-th entry of absolute path represented +// by segments. This function can be used to construct subsequent elements of +// the constructed path. The return value contains the newly created file +// descriptor or -1 on error. +func secureMkDir(fd int, segments []string, i int, perm os.FileMode, uid, gid int) (int, error) { + logger.Debugf("secure-mk-dir %d %q %d %v %d %d -> ...", fd, segments, i, perm, uid, gid) + + segment := segments[i] + made := true + var err error + var newFd int + + const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY + + if err = sysMkdirat(fd, segment, uint32(perm.Perm())); err != nil { + switch err { + case syscall.EEXIST: + made = false + case syscall.EROFS: + // Treat EROFS specially: this is a hint that we have to poke a + // hole using tmpfs. The path below is the location where we + // need to poke the hole. + p := "/" + strings.Join(segments[:i], "/") + return -1, &ReadOnlyFsError{Path: p} + default: + return -1, fmt.Errorf("cannot mkdir path segment %q: %v", segment, err) + } + } + newFd, err = sysOpenat(fd, segment, openFlags, 0) + if err != nil { + return -1, fmt.Errorf("cannot open path segment %q (got up to %q): %v", segment, + "/"+strings.Join(segments[:i], "/"), err) + } + if made { + // Chown each segment that we made. + if err := sysFchown(newFd, uid, gid); err != nil { + // Close the FD we opened if we fail here since the caller will get + // an error and won't assume responsibility for the FD. + sysClose(newFd) + return -1, fmt.Errorf("cannot chown path segment %q to %d.%d (got up to %q): %v", segment, uid, gid, + "/"+strings.Join(segments[:i], "/"), err) + } + } + logger.Debugf("secure-mk-dir %d %q %d %v %d %d -> %d", fd, segments, i, perm, uid, gid, newFd) + return newFd, err +} + +// secureMkFile creates a file at i-th entry of absolute path represented by +// segments. This function is meant to be used to create the leaf file as a +// preparation for a mount point. Existing files are reused without errors. +// Newly created files have the specified mode and ownership. +func secureMkFile(fd int, segments []string, i int, perm os.FileMode, uid, gid int) error { + logger.Debugf("secure-mk-file %d %q %d %v %d %d", fd, segments, i, perm, uid, gid) + segment := segments[i] + made := true + var newFd int + var err error + + // NOTE: Tests don't show O_RDONLY as has a value of 0 and is not + // translated to textual form. It is added here for explicitness. + const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_RDONLY + + // Open the final path segment as a file. Try to create the file (so that + // we know if we need to chown it) but fall back to just opening an + // existing one. + + newFd, err = sysOpenat(fd, segment, openFlags|syscall.O_CREAT|syscall.O_EXCL, uint32(perm.Perm())) + if err != nil { + switch err { + case syscall.EEXIST: + // If the file exists then just open it without O_CREAT and O_EXCL + newFd, err = sysOpenat(fd, segment, openFlags, 0) + if err != nil { + return fmt.Errorf("cannot open file %q: %v", segment, err) + } + made = false + case syscall.EROFS: + // Treat EROFS specially: this is a hint that we have to poke a + // hole using tmpfs. The path below is the location where we + // need to poke the hole. + p := "/" + strings.Join(segments[:i], "/") + return &ReadOnlyFsError{Path: p} + default: + return fmt.Errorf("cannot open file %q: %v", segment, err) + } + } + defer sysClose(newFd) + + if made { + // Chown the file if we made it. + if err := sysFchown(newFd, uid, gid); err != nil { + return fmt.Errorf("cannot chown file %q to %d.%d: %v", segment, uid, gid, err) + } + } + + return nil +} + +func splitIntoSegments(name string) ([]string, error) { + if name != filepath.Clean(name) { + return nil, fmt.Errorf("cannot split unclean path %q", name) + } + segments := strings.FieldsFunc(filepath.Clean(name), func(c rune) bool { return c == '/' }) + return segments, nil +} + +// SecureMkdirAll is the secure variant of os.MkdirAll. +// +// Unlike the regular version this implementation does not follow any symbolic +// links. At all times the new directory segment is created using mkdirat(2) +// while holding an open file descriptor to the parent directory. +// +// The only handled error is mkdirat(2) that fails with EEXIST. All other +// errors are fatal but there is no attempt to undo anything that was created. +// +// The uid and gid are used for the fchown(2) system call which is performed +// after each segment is created and opened. The special value -1 may be used +// to request that ownership is not changed. +func secureMkdirAll(name string, perm os.FileMode, uid, gid int) error { + logger.Debugf("secure-mkdir-all %q %v %d %d", name, perm, uid, gid) + + // Only support absolute paths to avoid bugs in snap-confine when + // called from anywhere. + if !filepath.IsAbs(name) { + return fmt.Errorf("cannot create directory with relative path: %q", name) + } + + // Split the path into segments. + segments, err := splitIntoSegments(name) + if err != nil { + return err + } + + // Create the prefix. + fd, err := secureMkPrefix(segments, perm, uid, gid) + if err != nil { + return err + } + defer sysClose(fd) + + if len(segments) > 0 { + // Create the final segment as a directory. + fd, err = secureMkDir(fd, segments, len(segments)-1, perm, uid, gid) + if err != nil { + return err + } + defer sysClose(fd) + } + + return nil +} + +// secureMkfileAll is a secure implementation of "mkdir -p $(dirname $1) && touch $1". +// +// This function is like secureMkdirAll but it creates an empty file instead of +// a directory for the final path component. Each created directory component +// is chowned to the desired user and group. +func secureMkfileAll(name string, perm os.FileMode, uid, gid int) error { + logger.Debugf("secure-mkfile-all %q %q %d %d", name, perm, uid, gid) + + // Only support absolute paths to avoid bugs in snap-confine when + // called from anywhere. + if !filepath.IsAbs(name) { + return fmt.Errorf("cannot create file with relative path: %q", name) + } + // Only support file names, not directory names. + if strings.HasSuffix(name, "/") { + return fmt.Errorf("cannot create non-file path: %q", name) + } + + // Split the path into segments. + segments, err := splitIntoSegments(name) + if err != nil { + return err + } + + // Create the prefix. + fd, err := secureMkPrefix(segments, perm, uid, gid) + if err != nil { + return err + } + defer sysClose(fd) + + if len(segments) > 0 { + // Create the final segment as a file. + err = secureMkFile(fd, segments, len(segments)-1, perm, uid, gid) + } + return err +} + +// planWritableMimic plans how to transform a given directory from read-only to writable. +// +// The algorithm is designed to be universally reversible so that it can be +// always de-constructed back to the original directory. The original directory +// is hidden by tmpfs and a subset of things that were present there originally +// is bind mounted back on top of empty directories or empty files. Symlinks +// are re-created directly. Devices and all other elements are not supported +// because they are forbidden in snaps for which this function is designed to +// be used with. Since the original directory is hidden the algorithm relies on +// a temporary directory where the original is bind-mounted during the +// progression of the algorithm. +func planWritableMimic(dir string) ([]*Change, error) { + // We need a place for "safe keeping" of what is present in the original + // directory as we are about to attach a tmpfs there, which will hide + // everything inside. + logger.Debugf("create-writable-mimic %q", dir) + safeKeepingDir := filepath.Join("/tmp/.snap/", dir) + + var changes []*Change + + // Bind mount the original directory elsewhere for safe-keeping. + changes = append(changes, &Change{ + Action: Mount, Entry: mount.Entry{ + // NOTE: Here we bind instead of recursively binding + // because recursive binds cannot be undone without + // parsing the mount table and exploring what is really + // there and this is not how the undo logic is + // designed. + Name: dir, Dir: safeKeepingDir, Options: []string{"bind"}}, + }) + // Mount tmpfs over the original directory, hiding its contents. + changes = append(changes, &Change{ + Action: Mount, Entry: mount.Entry{Name: "tmpfs", Dir: dir, Type: "tmpfs"}, + }) + // Iterate over the items in the original directory (nothing is mounted _yet_). + entries, err := ioutilReadDir(dir) + if err != nil { + return nil, err + } + for _, fi := range entries { + ch := &Change{Action: Mount, Entry: mount.Entry{ + Name: filepath.Join(safeKeepingDir, fi.Name()), + Dir: filepath.Join(dir, fi.Name()), + Options: []string{"bind"}, + }} + // Bind mount each element from the safe-keeping directory into the + // tmpfs. Our Change.Perform() engine can create the missing + // directories automatically so we don't bother creating those. + m := fi.Mode() + switch { + case m.IsDir(): + changes = append(changes, ch) + case m.IsRegular(): + ch.Entry.Options = append(ch.Entry.Options, "x-snapd.kind=file") + changes = append(changes, ch) + case m&os.ModeSymlink != 0: + if target, err := osReadlink(filepath.Join(dir, fi.Name())); err == nil { + ch.Entry.Options = append(ch.Entry.Options, "x-snapd.kind=symlink", fmt.Sprintf("x-snapd.symlink=%s", target)) + changes = append(changes, ch) + } + default: + logger.Noticef("skipping unsupported file %s", fi) + } + } + // Finally unbind the safe-keeping directory as we don't need it anymore. + changes = append(changes, &Change{ + Action: Unmount, Entry: mount.Entry{Name: "none", Dir: safeKeepingDir}, + }) + return changes, nil +} + +func ensureMountPoint(path string, mode os.FileMode, uid int, gid int) error { + // If the mount point is not present then create a directory in its + // place. This is very naive, doesn't handle read-only file systems + // but it is a good starting point for people working with things like + // $SNAP_DATA/subdirectory. + // + // We use lstat to ensure that we don't follow the symlink in case one + // was set up by the snap. Note that at the time this is run, all the + // snap's processes are frozen. + fi, err := osLstat(path) + switch { + case err != nil && os.IsNotExist(err): + return secureMkdirAll(path, mode, uid, gid) + case err != nil: + return fmt.Errorf("cannot inspect %q: %v", path, err) + case err == nil: + // Ensure that mount point is a directory. + if !fi.IsDir() { + return fmt.Errorf("cannot use %q for mounting, not a directory", path) + } + } + return nil +} diff --git a/cmd/snap-update-ns/utils_test.go b/cmd/snap-update-ns/utils_test.go new file mode 100644 index 00000000..71b3bd06 --- /dev/null +++ b/cmd/snap-update-ns/utils_test.go @@ -0,0 +1,538 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "bytes" + "os" + "path/filepath" + "syscall" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/testutil" +) + +type utilsSuite struct { + testutil.BaseTest + sys *update.SyscallRecorder + log *bytes.Buffer +} + +var _ = Suite(&utilsSuite{}) + +func (s *utilsSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.sys = &update.SyscallRecorder{} + s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) + buf, restore := logger.MockLogger() + s.BaseTest.AddCleanup(restore) + s.log = buf +} + +func (s *utilsSuite) TearDownTest(c *C) { + s.sys.CheckForStrayDescriptors(c) + s.BaseTest.TearDownTest(c) +} + +// secure-mkdir-all + +// Ensure that we reject unclean paths. +func (s *utilsSuite) TestSecureMkdirAllUnclean(c *C) { + err := update.SecureMkdirAll("/unclean//path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot split unclean path .*`) + c.Assert(s.sys.Calls(), HasLen, 0) +} + +// Ensure that we refuse to create a directory with an relative path. +func (s *utilsSuite) TestSecureMkdirAllRelative(c *C) { + err := update.SecureMkdirAll("rel/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot create directory with relative path: "rel/path"`) + c.Assert(s.sys.Calls(), HasLen, 0) +} + +// Ensure that we can "create the root directory. +func (s *utilsSuite) TestSecureMkdirAllLevel0(c *C) { + c.Assert(update.SecureMkdirAll("/", 0755, 123, 456), IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `close 3`, + }) +} + +// Ensure that we can create a directory in the top-level directory. +func (s *utilsSuite) TestSecureMkdirAllLevel1(c *C) { + os.Setenv("SNAPD_DEBUG", "1") + defer os.Unsetenv("SNAPD_DEBUG") + c.Assert(update.SecureMkdirAll("/path", 0755, 123, 456), IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "path" 0755`, + `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `fchown 4 123 456`, + `close 4`, + `close 3`, + }) + c.Assert(s.log.String(), testutil.Contains, `secure-mk-dir 3 ["path"] 0 -rwxr-xr-x 123 456 -> ...`) + c.Assert(s.log.String(), testutil.Contains, `secure-mk-dir 3 ["path"] 0 -rwxr-xr-x 123 456 -> 4`) +} + +// Ensure that we can create a directory two levels from the top-level directory. +func (s *utilsSuite) TestSecureMkdirAllLevel2(c *C) { + c.Assert(update.SecureMkdirAll("/path/to", 0755, 123, 456), IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "path" 0755`, + `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `fchown 4 123 456`, + `close 3`, + `mkdirat 4 "to" 0755`, + `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `fchown 3 123 456`, + `close 3`, + `close 4`, + }) +} + +// Ensure that we can create a directory three levels from the top-level directory. +func (s *utilsSuite) TestSecureMkdirAllLevel3(c *C) { + c.Assert(update.SecureMkdirAll("/path/to/something", 0755, 123, 456), IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "path" 0755`, + `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `fchown 4 123 456`, + `mkdirat 4 "to" 0755`, + `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 5 + `fchown 5 123 456`, + `close 4`, + `close 3`, + `mkdirat 5 "something" 0755`, + `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `fchown 3 123 456`, + `close 3`, + `close 5`, + }) +} + +// Ensure that we can detect read only filesystems. +func (s *utilsSuite) TestSecureMkdirAllROFS(c *C) { + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) // just realistic + s.sys.InsertFault(`mkdirat 4 "path" 0755`, syscall.EROFS) + err := update.SecureMkdirAll("/rofs/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) + c.Assert(err.(*update.ReadOnlyFsError).Path, Equals, "/rofs") + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "rofs" 0755`, // -> EEXIST + `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `close 3`, + `mkdirat 4 "path" 0755`, // -> EROFS + `close 4`, + }) +} + +// Ensure that we don't chown existing directories. +func (s *utilsSuite) TestSecureMkdirAllExistingDirsDontChown(c *C) { + s.sys.InsertFault(`mkdirat 3 "abs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 4 "path" 0755`, syscall.EEXIST) + err := update.SecureMkdirAll("/abs/path", 0755, 123, 456) + c.Assert(err, IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "abs" 0755`, + `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `close 3`, + `mkdirat 4 "path" 0755`, + `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `close 3`, + `close 4`, + }) +} + +// Ensure that we we close everything when mkdirat fails. +func (s *utilsSuite) TestSecureMkdirAllMkdiratError(c *C) { + s.sys.InsertFault(`mkdirat 3 "abs" 0755`, errTesting) + err := update.SecureMkdirAll("/abs", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot mkdir path segment "abs": testing`) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "abs" 0755`, + `close 3`, + }) +} + +// Ensure that we we close everything when fchown fails. +func (s *utilsSuite) TestSecureMkdirAllFchownError(c *C) { + s.sys.InsertFault(`fchown 4 123 456`, errTesting) + err := update.SecureMkdirAll("/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot chown path segment "path" to 123.456 \(got up to "/"\): testing`) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "path" 0755`, + `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `fchown 4 123 456`, + `close 4`, + `close 3`, + }) +} + +// Check error path when we cannot open root directory. +func (s *utilsSuite) TestSecureMkdirAllOpenRootError(c *C) { + s.sys.InsertFault(`open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) + err := update.SecureMkdirAll("/abs/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, "cannot open root directory: testing") + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> err + }) +} + +// Check error path when we cannot open non-root directory. +func (s *utilsSuite) TestSecureMkdirAllOpenError(c *C) { + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) + err := update.SecureMkdirAll("/abs/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot open path segment "abs" \(got up to "/"\): testing`) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "abs" 0755`, + `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> err + `close 3`, + }) +} + +func (s *utilsSuite) TestPlanWritableMimic(c *C) { + restore := update.MockReadDir(func(dir string) ([]os.FileInfo, error) { + c.Assert(dir, Equals, "/foo") + return []os.FileInfo{ + update.FakeFileInfo("file", 0), + update.FakeFileInfo("dir", os.ModeDir), + update.FakeFileInfo("symlink", os.ModeSymlink), + update.FakeFileInfo("error-symlink-readlink", os.ModeSymlink), + // NOTE: None of the filesystem entries below are supported because + // they cannot be placed inside snaps or can only be created at + // runtime in areas that are already writable and this would never + // have to be handled in a writable mimic. + update.FakeFileInfo("block-dev", os.ModeDevice), + update.FakeFileInfo("char-dev", os.ModeDevice|os.ModeCharDevice), + update.FakeFileInfo("socket", os.ModeSocket), + update.FakeFileInfo("pipe", os.ModeNamedPipe), + }, nil + }) + defer restore() + restore = update.MockReadlink(func(name string) (string, error) { + switch name { + case "/foo/symlink": + return "target", nil + case "/foo/error-symlink-readlink": + return "", errTesting + } + panic("unexpected") + }) + defer restore() + + changes, err := update.PlanWritableMimic("/foo") + c.Assert(err, IsNil) + c.Assert(changes, DeepEquals, []*update.Change{ + // Store /foo in /tmp/.snap/foo while we set things up + {Entry: mount.Entry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"bind"}}, Action: update.Mount}, + // Put a tmpfs over /foo + {Entry: mount.Entry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs"}, Action: update.Mount}, + // Bind mount files and directories over. Note that files are identified by x-snapd.kind=file option. + {Entry: mount.Entry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file"}}, Action: update.Mount}, + {Entry: mount.Entry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"bind"}}, Action: update.Mount}, + // Create symlinks. + // Bad symlinks and all other file types are skipped and not + // recorded in mount changes. + {Entry: mount.Entry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"bind", "x-snapd.kind=symlink", "x-snapd.symlink=target"}}, Action: update.Mount}, + // Unmount the safe-keeping directory + {Entry: mount.Entry{Name: "none", Dir: "/tmp/.snap/foo"}, Action: update.Unmount}, + }) +} + +func (s *utilsSuite) TestPlanWritableMimicErrors(c *C) { + restore := update.MockReadDir(func(dir string) ([]os.FileInfo, error) { + c.Assert(dir, Equals, "/foo") + return nil, errTesting + }) + defer restore() + restore = update.MockReadlink(func(name string) (string, error) { + return "", errTesting + }) + defer restore() + + changes, err := update.PlanWritableMimic("/foo") + c.Assert(err, ErrorMatches, "testing") + c.Assert(changes, HasLen, 0) +} + +// realSystemSuite is not isolated / mocked from the system. +type realSystemSuite struct{} + +var _ = Suite(&realSystemSuite{}) + +// Check that we can actually create directories. +// This doesn't test the chown logic as that requires root. +func (s *realSystemSuite) TestSecureMkdirAllForReal(c *C) { + d := c.MkDir() + + // Create d (which already exists) with mode 0777 (but c.MkDir() used 0700 + // internally and since we are not creating the directory we should not be + // changing that. + c.Assert(update.SecureMkdirAll(d, 0777, -1, -1), IsNil) + fi, err := os.Stat(d) + c.Assert(err, IsNil) + c.Check(fi.IsDir(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0700)) + + // Create d1, which is a simple subdirectory, with a distinct mode and + // check that it was applied. Note that default umask 022 is subtracted so + // effective directory has different permissions. + d1 := filepath.Join(d, "subdir") + c.Assert(update.SecureMkdirAll(d1, 0707, -1, -1), IsNil) + fi, err = os.Stat(d1) + c.Assert(err, IsNil) + c.Check(fi.IsDir(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0705)) + + // Create d2, which is a deeper subdirectory, with another distinct mode + // and check that it was applied. + d2 := filepath.Join(d, "subdir/subdir/subdir") + c.Assert(update.SecureMkdirAll(d2, 0750, -1, -1), IsNil) + fi, err = os.Stat(d2) + c.Assert(err, IsNil) + c.Check(fi.IsDir(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0750)) +} + +// secure-mkfile-all + +// Ensure that we reject unclean paths. +func (s *utilsSuite) TestSecureMkfileAllUnclean(c *C) { + err := update.SecureMkfileAll("/unclean//path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot split unclean path .*`) + c.Assert(s.sys.Calls(), HasLen, 0) +} + +// Ensure that we refuse to create a file with an relative path. +func (s *utilsSuite) TestSecureMkfileAllRelative(c *C) { + err := update.SecureMkfileAll("rel/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot create file with relative path: "rel/path"`) + c.Assert(s.sys.Calls(), HasLen, 0) +} + +// Ensure that we refuse creating the root directory as a file. +func (s *utilsSuite) TestSecureMkfileAllLevel0(c *C) { + err := update.SecureMkfileAll("/", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot create non-file path: "/"`) + c.Assert(s.sys.Calls(), HasLen, 0) +} + +// Ensure that we can create a file in the top-level directory. +func (s *utilsSuite) TestSecureMkfileAllLevel1(c *C) { + c.Assert(update.SecureMkfileAll("/path", 0755, 123, 456), IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, // -> 4 + `fchown 4 123 456`, + `close 4`, + `close 3`, + }) +} + +// Ensure that we can create a file two levels from the top-level directory. +func (s *utilsSuite) TestSecureMkfileAllLevel2(c *C) { + c.Assert(update.SecureMkfileAll("/path/to", 0755, 123, 456), IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "path" 0755`, + `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `fchown 4 123 456`, + `close 3`, + `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, // -> 3 + `fchown 3 123 456`, + `close 3`, + `close 4`, + }) +} + +// Ensure that we can create a file three levels from the top-level directory. +func (s *utilsSuite) TestSecureMkfileAllLevel3(c *C) { + c.Assert(update.SecureMkfileAll("/path/to/something", 0755, 123, 456), IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "path" 0755`, + `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `fchown 4 123 456`, + `mkdirat 4 "to" 0755`, + `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 5 + `fchown 5 123 456`, + `close 4`, + `close 3`, + `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, // -> 3 + `fchown 3 123 456`, + `close 3`, + `close 5`, + }) +} + +// Ensure that we can detect read only filesystems. +func (s *utilsSuite) TestSecureMkfileAllROFS(c *C) { + s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) // just realistic + s.sys.InsertFault(`openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EROFS) + err := update.SecureMkfileAll("/rofs/path", 0755, 123, 456) + c.Check(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) + c.Assert(err.(*update.ReadOnlyFsError).Path, Equals, "/rofs") + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "rofs" 0755`, // -> EEXIST + `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `close 3`, + `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, // -> EROFS + `close 4`, + }) +} + +// Ensure that we don't chown existing files or directories. +func (s *utilsSuite) TestSecureMkfileAllExistingDirsDontChown(c *C) { + s.sys.InsertFault(`mkdirat 3 "abs" 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EEXIST) + err := update.SecureMkfileAll("/abs/path", 0755, 123, 456) + c.Check(err, IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "abs" 0755`, + `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 4 + `close 3`, + `openat 4 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, // -> EEXIST + `openat 4 "path" O_NOFOLLOW|O_CLOEXEC 0`, // -> 3 + `close 3`, + `close 4`, + }) +} + +// Ensure that we we close everything when openat fails. +func (s *utilsSuite) TestSecureMkfileAllOpenat2ndError(c *C) { + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EEXIST) + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC 0`, errTesting) + err := update.SecureMkfileAll("/abs", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot open file "abs": testing`) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, // -> EEXIST + `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC 0`, // -> err + `close 3`, + }) +} + +// Ensure that we we close everything when openat (non-exclusive) fails. +func (s *utilsSuite) TestSecureMkfileAllOpenatError(c *C) { + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) + err := update.SecureMkfileAll("/abs", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot open file "abs": testing`) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, // -> err + `close 3`, + }) +} + +// Ensure that we we close everything when fchown fails. +func (s *utilsSuite) TestSecureMkfileAllFchownError(c *C) { + s.sys.InsertFault(`fchown 4 123 456`, errTesting) + err := update.SecureMkfileAll("/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot chown file "path" to 123.456: testing`) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, // -> 4 + `fchown 4 123 456`, + `close 4`, + `close 3`, + }) +} + +// Check error path when we cannot open root directory. +func (s *utilsSuite) TestSecureMkfileAllOpenRootError(c *C) { + s.sys.InsertFault(`open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) + err := update.SecureMkfileAll("/abs/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, "cannot open root directory: testing") + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> err + }) +} + +// Check error path when we cannot open non-root directory. +func (s *utilsSuite) TestSecureMkfileAllOpenError(c *C) { + s.sys.InsertFault(`openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, errTesting) + err := update.SecureMkfileAll("/abs/path", 0755, 123, 456) + c.Assert(err, ErrorMatches, `cannot open path segment "abs" \(got up to "/"\): testing`) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> 3 + `mkdirat 3 "abs" 0755`, + `openat 3 "abs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, // -> err + `close 3`, + }) +} + +// Check that we can actually create files. +// This doesn't test the chown logic as that requires root. +func (s *realSystemSuite) TestSecureMkfileAllForReal(c *C) { + d := c.MkDir() + + // Create f1, which is a simple subdirectory, with a distinct mode and + // check that it was applied. Note that default umask 022 is subtracted so + // effective directory has different permissions. + f1 := filepath.Join(d, "file") + c.Assert(update.SecureMkfileAll(f1, 0707, -1, -1), IsNil) + fi, err := os.Stat(f1) + c.Assert(err, IsNil) + c.Check(fi.Mode().IsRegular(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0705)) + + // Create f2, which is a deeper subdirectory, with another distinct mode + // and check that it was applied. + f2 := filepath.Join(d, "subdir/subdir/file") + c.Assert(update.SecureMkfileAll(f2, 0750, -1, -1), IsNil) + fi, err = os.Stat(f2) + c.Assert(err, IsNil) + c.Check(fi.Mode().IsRegular(), Equals, true) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0750)) +} + +func (s *utilsSuite) TestCleanTrailingSlash(c *C) { + // This is a sanity test for the use of filepath.Clean in secureMk{dir,file}All + c.Assert(filepath.Clean("/path/"), Equals, "/path") + c.Assert(filepath.Clean("path/"), Equals, "path") + c.Assert(filepath.Clean("path/."), Equals, "path") + c.Assert(filepath.Clean("path/.."), Equals, ".") + c.Assert(filepath.Clean("other/path/.."), Equals, "other") +} + +func (s *utilsSuite) TestSplitIntoSegments(c *C) { + sg, err := update.SplitIntoSegments("/foo/bar/froz") + c.Assert(err, IsNil) + c.Assert(sg, DeepEquals, []string{"foo", "bar", "froz"}) + + sg, err = update.SplitIntoSegments("/foo//fii/../.") + c.Assert(err, ErrorMatches, `cannot split unclean path ".+"`) + c.Assert(sg, HasLen, 0) +} diff --git a/cmd/snap/cmd_abort.go b/cmd/snap/cmd_abort.go new file mode 100644 index 00000000..27b6cbf3 --- /dev/null +++ b/cmd/snap/cmd_abort.go @@ -0,0 +1,60 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdAbort struct{ changeIDMixin } + +var shortAbortHelp = i18n.G("Abort a pending change") + +var longAbortHelp = i18n.G(` +The abort command attempts to abort a change that still has pending tasks. +`) + +func init() { + addCommand("abort", + shortAbortHelp, + longAbortHelp, + func() flags.Commander { + return &cmdAbort{} + }, + changeIDMixinOptDesc, + changeIDMixinArgDesc, + ) +} + +func (x *cmdAbort) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + id, err := x.GetChangeID(cli) + if err != nil { + return err + } + _, err = cli.Abort(id) + return err +} diff --git a/cmd/snap/cmd_ack.go b/cmd/snap/cmd_ack.go new file mode 100644 index 00000000..6f336fff --- /dev/null +++ b/cmd/snap/cmd_ack.go @@ -0,0 +1,77 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdAck struct { + AckOptions struct { + AssertionFile flags.Filename + } `positional-args:"true" required:"true"` +} + +var shortAckHelp = i18n.G("Adds an assertion to the system") +var longAckHelp = i18n.G(` +The ack command tries to add an assertion to the system assertion database. + +The assertion may also be a newer revision of a preexisting assertion that it +will replace. + +To succeed the assertion must be valid, its signature verified with a known +public key and the assertion consistent with and its prerequisite in the +database. +`) + +func init() { + addCommand("ack", shortAckHelp, longAckHelp, func() flags.Commander { + return &cmdAck{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Assertion file"), + }}) +} + +func ackFile(assertFile string) error { + assertData, err := ioutil.ReadFile(assertFile) + if err != nil { + return err + } + + return Client().Ack(assertData) +} + +func (x *cmdAck) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + if err := ackFile(string(x.AckOptions.AssertionFile)); err != nil { + return fmt.Errorf("cannot assert: %v", err) + } + return nil +} diff --git a/cmd/snap/cmd_alias.go b/cmd/snap/cmd_alias.go new file mode 100644 index 00000000..bc375422 --- /dev/null +++ b/cmd/snap/cmd_alias.go @@ -0,0 +1,115 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "text/tabwriter" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" +) + +type cmdAlias struct { + Positionals struct { + SnapApp appName `required:"yes"` + Alias string `required:"yes"` + } `positional-args:"true"` +} + +// TODO: implement a completer for snapApp + +var shortAliasHelp = i18n.G("Sets up a manual alias") +var longAliasHelp = i18n.G(` +The alias command aliases the given snap application to the given alias. + +Once this manual alias is setup the respective application command can be invoked just using the alias. +`) + +func init() { + addCommand("alias", shortAliasHelp, longAliasHelp, func() flags.Commander { + return &cmdAlias{} + }, nil, []argDesc{ + {name: ""}, + // TRANSLATORS: This needs to be wrapped in <>s. + {name: i18n.G("")}, + }) +} + +func (x *cmdAlias) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName, appName := snap.SplitSnapApp(string(x.Positionals.SnapApp)) + alias := x.Positionals.Alias + + cli := Client() + id, err := cli.Alias(snapName, appName, alias) + if err != nil { + return err + } + + chg, err := wait(cli, id) + if err != nil { + return err + } + if err := showAliasChanges(chg); err != nil { + return err + } + + return nil +} + +type changedAlias struct { + Snap string `json:"snap"` + App string `json:"app"` + Alias string `json:"alias"` +} + +func showAliasChanges(chg *client.Change) error { + var added, removed []*changedAlias + if err := chg.Get("aliases-added", &added); err != nil && err != client.ErrNoData { + return err + } + if err := chg.Get("aliases-removed", &removed); err != nil && err != client.ErrNoData { + return err + } + w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) + if len(added) != 0 { + printChangedAliases(w, i18n.G("Added"), added) + } + if len(removed) != 0 { + printChangedAliases(w, i18n.G("Removed"), removed) + } + w.Flush() + return nil +} + +func printChangedAliases(w io.Writer, label string, changed []*changedAlias) { + fmt.Fprintf(w, "%s:\n", label) + for _, a := range changed { + fmt.Fprintf(w, "\t- %s as %s\n", snap.JoinSnapApp(a.Snap, a.App), a.Alias) + } +} diff --git a/cmd/snap/cmd_alias_test.go b/cmd/snap/cmd_alias_test.go new file mode 100644 index 00000000..68ab900c --- /dev/null +++ b/cmd/snap/cmd_alias_test.go @@ -0,0 +1,78 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestAliasHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] alias [] [] + +The alias command aliases the given snap application to the given alias. + +Once this manual alias is setup the respective application command can be +invoked just using the alias. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"alias", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestAlias(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/aliases": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "alias", + "snap": "alias-snap", + "app": "cmd1", + "alias": "alias1", + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done", "data": {"aliases-added": [{"alias": "alias1", "snap": "alias-snap", "app": "cmd1"}]}}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"alias", "alias-snap.cmd1", "alias1"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, ""+ + "Added:\n"+ + " - alias-snap.cmd1 as alias1\n", + ) + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_aliases.go b/cmd/snap/cmd_aliases.go new file mode 100644 index 00000000..fecbde13 --- /dev/null +++ b/cmd/snap/cmd_aliases.go @@ -0,0 +1,133 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "sort" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdAliases struct { + Positionals struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"true"` +} + +var shortAliasesHelp = i18n.G("Lists aliases in the system") +var longAliasesHelp = i18n.G(` +The aliases command lists all aliases available in the system and their status. + +$ snap aliases + +Lists only the aliases defined by the specified snap. + +An alias noted as undefined means it was explicitly enabled or disabled but is +not defined in the current revision of the snap; possibly temporarely (e.g +because of a revert), if not this can be cleared with snap alias --reset. +`) + +func init() { + addCommand("aliases", shortAliasesHelp, longAliasesHelp, func() flags.Commander { + return &cmdAliases{} + }, nil, nil) +} + +type aliasInfo struct { + Snap string + Command string + Alias string + Status string + Auto string +} + +type aliasInfos []*aliasInfo + +func (infos aliasInfos) Len() int { return len(infos) } +func (infos aliasInfos) Swap(i, j int) { infos[i], infos[j] = infos[j], infos[i] } +func (infos aliasInfos) Less(i, j int) bool { + if infos[i].Snap < infos[j].Snap { + return true + } + if infos[i].Snap == infos[j].Snap { + if infos[i].Command < infos[j].Command { + return true + } + if infos[i].Command == infos[j].Command { + if infos[i].Alias < infos[j].Alias { + return true + } + } + } + return false +} + +func (x *cmdAliases) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + allStatuses, err := Client().Aliases() + if err == nil { + w := tabWriter() + fmt.Fprintln(w, i18n.G("Command\tAlias\tNotes")) + defer w.Flush() + var infos aliasInfos + filterSnap := string(x.Positionals.Snap) + if filterSnap != "" { + allStatuses = map[string]map[string]client.AliasStatus{ + filterSnap: allStatuses[filterSnap], + } + } + for snapName, aliasStatuses := range allStatuses { + for alias, aliasStatus := range aliasStatuses { + infos = append(infos, &aliasInfo{ + Snap: snapName, + Command: aliasStatus.Command, + Alias: alias, + Status: aliasStatus.Status, + Auto: aliasStatus.Auto, + }) + } + } + sort.Sort(infos) + + for _, info := range infos { + var notes []string + if info.Status != "auto" { + notes = append(notes, info.Status) + if info.Status == "manual" && info.Auto != "" { + notes = append(notes, "override") + } + } + notesStr := strings.Join(notes, ",") + if notesStr == "" { + notesStr = "-" + } + fmt.Fprintf(w, "%s\t%s\t%s\n", info.Command, info.Alias, notesStr) + } + } + return err +} diff --git a/cmd/snap/cmd_aliases_test.go b/cmd/snap/cmd_aliases_test.go new file mode 100644 index 00000000..58a81b0a --- /dev/null +++ b/cmd/snap/cmd_aliases_test.go @@ -0,0 +1,168 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "net/http" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestAliasesHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] aliases [] + +The aliases command lists all aliases available in the system and their status. + +$ snap aliases + +Lists only the aliases defined by the specified snap. + +An alias noted as undefined means it was explicitly enabled or disabled but is +not defined in the current revision of the snap; possibly temporarely (e.g +because of a revert), if not this can be cleared with snap alias --reset. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"aliases", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestAliases(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/aliases") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": map[string]map[string]client.AliasStatus{ + "foo": { + "foo0": {Command: "foo", Status: "auto", Auto: "foo"}, + "foo_reset": {Command: "foo.reset", Manual: "reset", Status: "manual"}, + }, + "bar": { + "bar_dump": {Command: "bar.dump", Status: "manual", Manual: "dump"}, + "bar_dump.1": {Command: "bar.dump", Status: "disabled", Auto: "dump"}, + "bar_restore": {Command: "bar.safe-restore", Status: "manual", Auto: "restore", Manual: "safe-restore"}, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"aliases"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Command Alias Notes\n" + + "bar.dump bar_dump manual\n" + + "bar.dump bar_dump.1 disabled\n" + + "bar.safe-restore bar_restore manual,override\n" + + "foo foo0 -\n" + + "foo.reset foo_reset manual\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAliasesFilterSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/aliases") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": map[string]map[string]client.AliasStatus{ + "foo": { + "foo0": {Command: "foo", Status: "auto", Auto: "foo"}, + "foo_reset": {Command: "foo.reset", Manual: "reset", Status: "manual"}, + }, + "bar": { + "bar_dump": {Command: "bar.dump", Status: "manual", Manual: "dump"}, + "bar_dump.1": {Command: "bar.dump", Status: "disabled", Auto: "dump"}, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"aliases", "foo"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Command Alias Notes\n" + + "foo foo0 -\n" + + "foo.reset foo_reset manual\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAliasesNone(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/aliases") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": map[string]map[string]client.AliasStatus{}, + }) + }) + _, err := Parser().ParseArgs([]string{"aliases"}) + c.Assert(err, IsNil) + expectedStdout := "" + + "Command Alias Notes\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAliasesSorting(c *C) { + tests := []struct { + snap1 string + cmd1 string + alias1 string + snap2 string + cmd2 string + alias2 string + }{ + {"bar", "bar", "r", "baz", "baz", "z"}, + {"bar", "bar", "bar0", "bar", "bar.app", "bapp"}, + {"bar", "bar.app1", "bapp1", "bar", "bar.app2", "bapp2"}, + {"bar", "bar.app1", "appx", "bar", "bar.app1", "appy"}, + } + + for _, test := range tests { + res := AliasInfoLess(test.snap1, test.alias1, test.cmd1, test.snap2, test.alias2, test.cmd2) + c.Check(res, Equals, true, Commentf("%v", test)) + + rres := AliasInfoLess(test.snap2, test.alias2, test.cmd2, test.snap1, test.alias1, test.cmd1) + c.Check(rres, Equals, false, Commentf("reversed %v", test)) + } + +} diff --git a/cmd/snap/cmd_auto_import.go b/cmd/snap/cmd_auto_import.go new file mode 100644 index 00000000..3712024a --- /dev/null +++ b/cmd/snap/cmd_auto_import.go @@ -0,0 +1,300 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "bufio" + "crypto" + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +const autoImportsName = "auto-import.assert" + +var mountInfoPath = "/proc/self/mountinfo" + +func autoImportCandidates() ([]string, error) { + var cands []string + + // see https://www.kernel.org/doc/Documentation/filesystems/proc.txt, + // sec. 3.5 + f, err := os.Open(mountInfoPath) + if err != nil { + return nil, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + l := strings.Fields(scanner.Text()) + + // Per proc.txt:3.5, /proc//mountinfo looks like + // + // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + // + // and (7) has zero or more elements, find the "-" separator. + i := 6 + for i < len(l) && l[i] != "-" { + i++ + } + if i+2 >= len(l) { + continue + } + + mountSrc := l[i+2] + + // skip everything that is not a device (cgroups, debugfs etc) + if !strings.HasPrefix(mountSrc, "/dev/") { + continue + } + // skip all loop devices (snaps) + if strings.HasPrefix(mountSrc, "/dev/loop") { + continue + } + // skip all ram disks (unless in tests) + if !osutil.GetenvBool("SNAPPY_TESTING") && strings.HasPrefix(mountSrc, "/dev/ram") { + continue + } + + mountPoint := l[4] + cand := filepath.Join(mountPoint, autoImportsName) + if osutil.FileExists(cand) { + cands = append(cands, cand) + } + } + + return cands, scanner.Err() + +} + +func queueFile(src string) error { + // refuse huge files, this is for assertions + fi, err := os.Stat(src) + if err != nil { + return err + } + // 640kb ought be to enough for anyone + if fi.Size() > 640*1024 { + msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size()) + logger.Noticef("error: %v", msg) + return msg + } + + // ensure name is predictable, weak hash is ok + hash, _, err := osutil.FileDigest(src, crypto.SHA3_384) + if err != nil { + return err + } + + dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash))) + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + + return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite) +} + +func autoImportFromSpool() (added int, err error) { + files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) + if os.IsNotExist(err) { + return 0, nil + } + if err != nil { + return 0, err + } + + for _, fi := range files { + cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name()) + if err := ackFile(cand); err != nil { + logger.Noticef("error: cannot import %s: %s", cand, err) + continue + } else { + logger.Noticef("imported %s", cand) + added++ + } + // FIXME: only remove stuff older than N days? + if err := os.Remove(cand); err != nil { + return 0, err + } + } + + return added, nil +} + +func autoImportFromAllMounts() (int, error) { + cands, err := autoImportCandidates() + if err != nil { + return 0, err + } + + added := 0 + for _, cand := range cands { + err := ackFile(cand) + // the server is not ready yet + if _, ok := err.(client.ConnectionError); ok { + logger.Noticef("queuing for later %s", cand) + if err := queueFile(cand); err != nil { + return 0, err + } + continue + } + if err != nil { + logger.Noticef("error: cannot import %s: %s", cand, err) + continue + } else { + logger.Noticef("imported %s", cand) + } + added++ + } + + return added, nil +} + +func tryMount(deviceName string) (string, error) { + tmpMountTarget, err := ioutil.TempDir("", "snapd-auto-import-mount-") + if err != nil { + err = fmt.Errorf("cannot create temporary mount point: %v", err) + logger.Noticef("error: %v", err) + return "", err + } + // udev does not provide much environment ;) + if os.Getenv("PATH") == "" { + os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") + } + // not using syscall.Mount() because we don't know the fs type in advance + cmd := exec.Command("mount", "-t", "ext4,vfat", "-o", "ro", "--make-private", deviceName, tmpMountTarget) + if output, err := cmd.CombinedOutput(); err != nil { + os.Remove(tmpMountTarget) + err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err)) + logger.Noticef("error: %v", err) + return "", err + } + + return tmpMountTarget, nil +} + +func doUmount(mp string) error { + if err := syscall.Unmount(mp, 0); err != nil { + return err + } + return os.Remove(mp) +} + +type cmdAutoImport struct { + Mount []string `long:"mount" arg-name:""` + + ForceClassic bool `long:"force-classic"` +} + +var shortAutoImportHelp = i18n.G("Inspects devices for actionable information") + +var longAutoImportHelp = i18n.G(` +The auto-import command searches available mounted devices looking for +assertions that are signed by trusted authorities, and potentially +performs system changes based on them. + +If one or more device paths are provided via --mount, these are temporariy +mounted to be inspected as well. Even in that case the command will still +consider all available mounted devices for inspection. + +Imported assertions must be made available in the auto-import.assert file +in the root of the filesystem. +`) + +func init() { + cmd := addCommand("auto-import", + shortAutoImportHelp, + longAutoImportHelp, + func() flags.Commander { + return &cmdAutoImport{} + }, map[string]string{ + "mount": i18n.G("Temporarily mount device before inspecting"), + "force-classic": i18n.G("Force import on classic systems"), + }, nil) + cmd.hidden = true +} + +func autoAddUsers() error { + cmd := cmdCreateUser{ + Known: true, Sudoer: true, + } + return cmd.Execute(nil) +} + +func (x *cmdAutoImport) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if release.OnClassic && !x.ForceClassic { + fmt.Fprintf(Stderr, "auto-import is disabled on classic\n") + return nil + } + + for _, path := range x.Mount { + // udev adds new /dev/loopX devices on the fly when a + // loop mount happens and there is no loop device left. + // + // We need to ignore these events because otherwise both + // our mount and the "mount -o loop" fight over the same + // device and we get nasty errors + if strings.HasPrefix(path, "/dev/loop") { + continue + } + + mp, err := tryMount(path) + if err != nil { + continue // Error was reported. Continue looking. + } + defer doUmount(mp) + } + + added1, err := autoImportFromSpool() + if err != nil { + return err + } + + added2, err := autoImportFromAllMounts() + if err != nil { + return err + } + + if added1+added2 > 0 { + return autoAddUsers() + } + + return nil +} diff --git a/cmd/snap/cmd_auto_import_test.go b/cmd/snap/cmd_auto_import_test.go new file mode 100644 index 00000000..a05d70da --- /dev/null +++ b/cmd/snap/cmd_auto_import_test.go @@ -0,0 +1,302 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/release" +) + +func makeMockMountInfo(c *C, content string) string { + fn := filepath.Join(c.MkDir(), "mountinfo") + err := ioutil.WriteFile(fn, []byte(content), 0644) + c.Assert(err, IsNil) + return fn +} + +func (s *SnapSuite) TestAutoImportAssertsHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + fakeAssertData := []byte("my-assertion") + + n := 0 + total := 2 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/assertions") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(postData, DeepEquals, fakeAssertData) + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) + n++ + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/create-user") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`) + + fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) + n++ + default: + c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n) + } + + }) + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + logbuf, restore := logger.MockLogger() + defer restore() + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `created user "foo"`+"\n") + // matches because we may get a: + // "WARNING: cannot create syslog logger\n" + // in the output + c.Check(logbuf.String(), Matches, fmt.Sprintf("(?ms).*imported %s\n", fakeAssertsFn)) + c.Check(n, Equals, total) +} + +func (s *SnapSuite) TestAutoImportAssertsNotImportedFromLoop(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + fakeAssertData := []byte("bad-assertion") + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + // assertion is ignored, nothing is posted to this endpoint + panic("not reached") + }) + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmtWithLoop := ` +24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/loop1 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmtWithLoop, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestAutoImportCandidatesHappy(c *C) { + dirs := make([]string, 4) + args := make([]interface{}, len(dirs)) + files := make([]string, len(dirs)) + for i := range dirs { + dirs[i] = c.MkDir() + args[i] = dirs[i] + files[i] = filepath.Join(dirs[i], "auto-import.assert") + err := ioutil.WriteFile(files[i], nil, 0644) + c.Assert(err, IsNil) + } + + mockMountInfoFmtWithLoop := ` +too short +24 0 8:18 / %[1]s rw,relatime foo ext3 /dev/meep2 no,separator +24 0 8:18 / %[2]s rw,relatime - ext3 /dev/meep2 rw,errors=remount-ro,data=ordered +24 0 8:18 / %[3]s rw,relatime opt:1 - ext4 /dev/meep3 rw,errors=remount-ro,data=ordered +24 0 8:18 / %[4]s rw,relatime opt:1 opt:2 - ext2 /dev/meep1 rw,errors=remount-ro,data=ordered +` + + content := fmt.Sprintf(mockMountInfoFmtWithLoop, args...) + restore := snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + l, err := snap.AutoImportCandidates() + c.Check(err, IsNil) + c.Check(l, DeepEquals, files[1:]) +} + +func (s *SnapSuite) TestAutoImportAssertsHappyNotOnClassic(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + fakeAssertData := []byte("my-assertion") + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Errorf("auto-import on classic is disabled, but something tried to do a %q with %s", r.Method, r.URL.Path) + }) + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "auto-import is disabled on classic\n") +} + +func (s *SnapSuite) TestAutoImportIntoSpool(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + logbuf, restore := logger.MockLogger() + defer restore() + + fakeAssertData := []byte("good-assertion") + + // ensure we can not connect + snap.ClientConfig.BaseURL = "can-not-connect-to-this-url" + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/sc1 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + // matches because we may get a: + // "WARNING: cannot create syslog logger\n" + // in the output + c.Check(logbuf.String(), Matches, "(?ms).*queuing for later.*\n") + + files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) + c.Assert(err, IsNil) + c.Check(files, HasLen, 1) + c.Check(files[0].Name(), Equals, "iOkaeet50rajLvL-0Qsf2ELrTdn3XIXRIBlDewcK02zwRi3_TJlUOTl9AaiDXmDn.assert") +} + +func (s *SnapSuite) TestAutoImportFromSpoolHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + fakeAssertData := []byte("my-assertion") + + n := 0 + total := 2 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/assertions") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(postData, DeepEquals, fakeAssertData) + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) + n++ + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/create-user") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`) + + fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) + n++ + default: + c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n) + } + + }) + + fakeAssertsFn := filepath.Join(dirs.SnapAssertsSpoolDir, "1234343") + err := os.MkdirAll(filepath.Dir(fakeAssertsFn), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + logbuf, restore := logger.MockLogger() + defer restore() + + rest, err := snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `created user "foo"`+"\n") + // matches because we may get a: + // "WARNING: cannot create syslog logger\n" + // in the output + c.Check(logbuf.String(), Matches, fmt.Sprintf("(?ms).*imported %s\n", fakeAssertsFn)) + c.Check(n, Equals, total) + + c.Check(osutil.FileExists(fakeAssertsFn), Equals, false) +} + +func (s *SnapSuite) TestAutoImportIntoSpoolUnhappyTooBig(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + _, restoreLogger := logger.MockLogger() + defer restoreLogger() + + // fake data is bigger than the default assertion limit + fakeAssertData := make([]byte, 641*1024) + + // ensure we can not connect + snap.ClientConfig.BaseURL = "can-not-connect-to-this-url" + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - squashfs /dev/sc1 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + _, err = snap.Parser().ParseArgs([]string{"auto-import"}) + c.Assert(err, ErrorMatches, "cannot queue .*, file size too big: 656384") +} diff --git a/cmd/snap/cmd_booted.go b/cmd/snap/cmd_booted.go new file mode 100644 index 00000000..4cc6dc64 --- /dev/null +++ b/cmd/snap/cmd_booted.go @@ -0,0 +1,50 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdBooted struct{} + +func init() { + cmd := addCommand("booted", + "internal", + "internal", + func() flags.Commander { + return &cmdBooted{} + }, nil, nil) + cmd.hidden = true +} + +// WARNING: do not remove this command, older systems may still have +// a systemd snapd.firstboot.service job in /etc/systemd/system +// that we did not cleanup. so we need this dummy command or +// those units will start failing. +func (x *cmdBooted) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + fmt.Fprintf(Stderr, "booted command is deprecated\n") + return nil +} diff --git a/cmd/snap/cmd_buy.go b/cmd/snap/cmd_buy.go new file mode 100644 index 00000000..63289de6 --- /dev/null +++ b/cmd/snap/cmd_buy.go @@ -0,0 +1,138 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/store" + + "github.com/jessevdk/go-flags" +) + +var shortBuyHelp = i18n.G("Buys a snap") +var longBuyHelp = i18n.G(` +The buy command buys a snap from the store. +`) + +type cmdBuy struct { + Positional struct { + SnapName remoteSnapName + } `positional-args:"yes" required:"yes"` +} + +func init() { + addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander { + return &cmdBuy{} + }, map[string]string{}, []argDesc{{ + name: "", + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Snap name"), + }}) +} + +func (x *cmdBuy) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return buySnap(string(x.Positional.SnapName)) +} + +func buySnap(snapName string) error { + cli := Client() + + user := cli.LoggedInUser() + if user == nil { + return fmt.Errorf(i18n.G("You need to be logged in to purchase software. Please run 'snap login' and try again.")) + } + + if strings.ContainsAny(snapName, ":*") { + return fmt.Errorf(i18n.G("cannot buy snap: invalid characters in name")) + } + + snap, resultInfo, err := cli.FindOne(snapName) + if err != nil { + return err + } + + opts := &store.BuyOptions{ + SnapID: snap.ID, + Currency: resultInfo.SuggestedCurrency, + } + + opts.Price, opts.Currency, err = getPrice(snap.Prices, opts.Currency) + if err != nil { + return fmt.Errorf(i18n.G("cannot buy snap: %v"), err) + } + + if snap.Status == "available" { + return fmt.Errorf(i18n.G("cannot buy snap: it has already been bought")) + } + + err = cli.ReadyToBuy() + if err != nil { + if e, ok := err.(*client.Error); ok { + switch e.Kind { + case client.ErrorKindNoPaymentMethods: + return fmt.Errorf(i18n.G(`You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. + +Once you’ve added your payment details, you just need to run 'snap buy %s' again.`), snap.Name) + case client.ErrorKindTermsNotAccepted: + return fmt.Errorf(i18n.G(`In order to buy %q, you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. + +Once completed, return here and run 'snap buy %s' again.`), snap.Name, snap.Name) + } + } + return err + } + + // TRANSLATORS: %q, %q and %s are the snap name, developer, and price. Please wrap the translation at 80 characters. + fmt.Fprintf(Stdout, i18n.G(`Please re-enter your Ubuntu One password to purchase %q from %q +for %s. Press ctrl-c to cancel.`), snap.Name, snap.Developer, formatPrice(opts.Price, opts.Currency)) + fmt.Fprint(Stdout, "\n") + + err = requestLogin(user.Email) + if err != nil { + return err + } + + _, err = cli.Buy(opts) + if err != nil { + if e, ok := err.(*client.Error); ok { + switch e.Kind { + case client.ErrorKindPaymentDeclined: + return fmt.Errorf(i18n.G(`Sorry, your payment method has been declined by the issuer. Please review your +payment details at https://my.ubuntu.com/payment/edit and try again.`)) + } + } + return err + } + + // TRANSLATORS: %q and %s are the same snap name. Please wrap the translation at 80 characters. + fmt.Fprintf(Stdout, i18n.G(`Thanks for purchasing %q. You may now install it on any of your devices +with 'snap install %s'.`), snapName, snapName) + fmt.Fprint(Stdout, "\n") + + return nil +} diff --git a/cmd/snap/cmd_buy_test.go b/cmd/snap/cmd_buy_test.go new file mode 100644 index 00000000..c27ec71f --- /dev/null +++ b/cmd/snap/cmd_buy_test.go @@ -0,0 +1,450 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +type BuySnapSuite struct { + BaseSnapSuite +} + +var _ = check.Suite(&BuySnapSuite{}) + +type expectedURL struct { + Body string + Checker func(r *http.Request) + + callCount int +} + +type expectedMethod map[string]*expectedURL + +type expectedMethods map[string]*expectedMethod + +type buyTestMockSnapServer struct { + ExpectedMethods expectedMethods + + Checker *check.C +} + +func (s *buyTestMockSnapServer) serveHttp(w http.ResponseWriter, r *http.Request) { + method := s.ExpectedMethods[r.Method] + if method == nil || len(*method) == 0 { + s.Checker.Fatalf("unexpected HTTP method %s", r.Method) + } + + url := (*method)[r.URL.Path] + if url == nil { + s.Checker.Fatalf("unexpected URL %q", r.URL.Path) + } + + if url.Checker != nil { + url.Checker(r) + } + fmt.Fprintln(w, url.Body) + url.callCount++ +} + +func (s *buyTestMockSnapServer) checkCounts() { + for _, method := range s.ExpectedMethods { + for _, url := range *method { + s.Checker.Check(url.callCount, check.Equals, 1) + } + } +} + +func (s *BuySnapSuite) SetUpTest(c *check.C) { + s.BaseSnapSuite.SetUpTest(c) + s.Login(c) +} + +func (s *BuySnapSuite) TearDownTest(c *check.C) { + s.Logout(c) + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *BuySnapSuite) TestBuyHelp(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"buy"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "the required argument `` was not provided") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *BuySnapSuite) TestBuyInvalidCharacters(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"buy", "a:b"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser().ParseArgs([]string{"buy", "c*d"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +const buyFreeSnapFailsFindJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *BuySnapSuite) TestBuyFreeSnapFails(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": &expectedURL{ + Body: buyFreeSnapFailsFindJson, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap: snap is free") + c.Assert(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +const buySnapFindJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "priced", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10", + "prices": {"USD": 3.99, "GBP": 2.99} + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func buySnapFindURL(c *check.C) *expectedURL { + return &expectedURL{ + Body: buySnapFindJson, + Checker: func(r *http.Request) { + c.Check(r.URL.Query().Get("name"), check.Equals, "hello") + }, + } +} + +const buyReadyJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": true, + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func buyReady(c *check.C) *expectedURL { + return &expectedURL{ + Body: buyReadyJson, + } +} + +const buySnapJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": { + "state": "Complete" + }, + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +const loginJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": { + "id": 1, + "username": "username", + "email": "hello@mail.com", + "macaroon": "1234abcd", + "discharges": ["a", "b", "c"] + }, + "sources": [ + "store" + ] +} +` + +func (s *BuySnapSuite) TestBuySnapSuccess(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": buySnapFindURL(c), + "/v2/buy/ready": buyReady(c), + }, + "POST": &expectedMethod{ + "/v2/login": &expectedURL{ + Body: loginJson, + }, + "/v2/buy": &expectedURL{ + Body: buySnapJson, + Checker: func(r *http.Request) { + var postData struct { + SnapID string `json:"snap-id"` + Price float64 `json:"price"` + Currency string `json:"currency"` + } + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&postData) + c.Assert(err, check.IsNil) + + c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") + c.Check(postData.Price, check.Equals, 2.99) + c.Check(postData.Currency, check.Equals, "GBP") + }, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + // Confirm the purchase. + s.password = "the password" + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Check(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" +for 2.99GBP. Press ctrl-c to cancel. +Password of "hello@mail.com": +Thanks for purchasing "hello". You may now install it on any of your devices +with 'snap install hello'. +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const buySnapPaymentDeclinedJson = ` +{ + "type": "error", + "result": { + "message": "payment declined", + "kind": "payment-declined" + }, + "status-code": 400 +} +` + +func (s *BuySnapSuite) TestBuySnapPaymentDeclined(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": buySnapFindURL(c), + "/v2/buy/ready": buyReady(c), + }, + "POST": &expectedMethod{ + "/v2/login": &expectedURL{ + Body: loginJson, + }, + "/v2/buy": &expectedURL{ + Body: buySnapPaymentDeclinedJson, + Checker: func(r *http.Request) { + var postData struct { + SnapID string `json:"snap-id"` + Price float64 `json:"price"` + Currency string `json:"currency"` + } + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&postData) + c.Assert(err, check.IsNil) + + c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") + c.Check(postData.Price, check.Equals, 2.99) + c.Check(postData.Currency, check.Equals, "GBP") + }, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + // Confirm the purchase. + s.password = "the password" + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `Sorry, your payment method has been declined by the issuer. Please review your +payment details at https://my.ubuntu.com/payment/edit and try again.`) + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" +for 2.99GBP. Press ctrl-c to cancel. +Password of "hello@mail.com": +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const readyToBuyNoPaymentMethodJson = ` +{ + "type": "error", + "result": { + "message": "no payment methods", + "kind": "no-payment-methods" + }, + "status-code": 400 +}` + +func (s *BuySnapSuite) TestBuySnapFailsNoPaymentMethod(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": buySnapFindURL(c), + "/v2/buy/ready": &expectedURL{ + Body: readyToBuyNoPaymentMethodJson, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. + +Once you’ve added your payment details, you just need to run 'snap buy hello' again.`) + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +const readyToBuyNotAcceptedTermsJson = ` +{ + "type": "error", + "result": { + "message": "terms of service not accepted", + "kind": "terms-not-accepted" + }, + "status-code": 400 +}` + +func (s *BuySnapSuite) TestBuySnapFailsNotAcceptedTerms(c *check.C) { + mockServer := &buyTestMockSnapServer{ + ExpectedMethods: expectedMethods{ + "GET": &expectedMethod{ + "/v2/find": buySnapFindURL(c), + "/v2/buy/ready": &expectedURL{ + Body: readyToBuyNotAcceptedTermsJson, + }, + }, + }, + Checker: c, + } + defer mockServer.checkCounts() + s.RedirectClientToTestServer(mockServer.serveHttp) + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, `In order to buy "hello", you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. + +Once completed, return here and run 'snap buy hello' again.`) + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *BuySnapSuite) TestBuyFailsWithoutLogin(c *check.C) { + // We don't login here + s.Logout(c) + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Check(err, check.NotNil) + c.Check(err.Error(), check.Equals, "You need to be logged in to purchase software. Please run 'snap login' and try again.") + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_can_manage_refreshes.go b/cmd/snap/cmd_can_manage_refreshes.go new file mode 100644 index 00000000..a1295cce --- /dev/null +++ b/cmd/snap/cmd_can_manage_refreshes.go @@ -0,0 +1,51 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdCanManageRefreshes struct{} + +func init() { + cmd := addDebugCommand("can-manage-refreshes", + "(internal) return if refresh.schedule=managed can be used", + "(internal) return if refresh.schedule=managed can be used", + func() flags.Commander { + return &cmdCanManageRefreshes{} + }) + cmd.hidden = true +} + +func (x *cmdCanManageRefreshes) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + var resp bool + if err := Client().Debug("can-manage-refreshes", nil, &resp); err != nil { + return err + } + fmt.Fprintf(Stdout, "%v\n", resp) + return nil +} diff --git a/cmd/snap/cmd_changes.go b/cmd/snap/cmd_changes.go new file mode 100644 index 00000000..010a09ed --- /dev/null +++ b/cmd/snap/cmd_changes.go @@ -0,0 +1,168 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "regexp" + "sort" + "time" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortChangesHelp = i18n.G("List system changes") +var shortTasksHelp = i18n.G("List a change's tasks") +var longChangesHelp = i18n.G(` +The changes command displays a summary of the recent system changes performed.`) +var longTasksHelp = i18n.G(` +The tasks command displays a summary of tasks associated to an individual change.`) + +type cmdChanges struct { + Positional struct { + Snap string `positional-arg-name:""` + } `positional-args:"yes"` +} + +type cmdTasks struct{ changeIDMixin } + +func init() { + addCommand("changes", shortChangesHelp, longChangesHelp, + func() flags.Commander { return &cmdChanges{} }, nil, nil) + addCommand("tasks", shortTasksHelp, longTasksHelp, + func() flags.Commander { return &cmdTasks{} }, + changeIDMixinOptDesc, + changeIDMixinArgDesc).alias = "change" +} + +type changesByTime []*client.Change + +func (s changesByTime) Len() int { return len(s) } +func (s changesByTime) Less(i, j int) bool { return s[i].SpawnTime.Before(s[j].SpawnTime) } +func (s changesByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +var allDigits = regexp.MustCompile(`^[0-9]+$`).MatchString + +func (c *cmdChanges) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if allDigits(c.Positional.Snap) { + // TRANSLATORS: the %s is the argument given by the user to "snap changes" + return fmt.Errorf(i18n.G(`"snap changes" command expects a snap name, try: "snap tasks %s"`), c.Positional.Snap) + } + + if c.Positional.Snap == "everything" { + fmt.Fprintln(Stdout, i18n.G("Yes, yes it does.")) + return nil + } + + opts := client.ChangesOptions{ + SnapName: c.Positional.Snap, + Selector: client.ChangesAll, + } + + cli := Client() + changes, err := cli.Changes(&opts) + if err != nil { + return err + } + + if len(changes) == 0 { + return fmt.Errorf(i18n.G("no changes found")) + } + + sort.Sort(changesByTime(changes)) + + w := tabWriter() + + fmt.Fprintf(w, i18n.G("ID\tStatus\tSpawn\tReady\tSummary\n")) + for _, chg := range changes { + spawnTime := chg.SpawnTime.UTC().Format(time.RFC3339) + readyTime := chg.ReadyTime.UTC().Format(time.RFC3339) + if chg.ReadyTime.IsZero() { + readyTime = "-" + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", chg.ID, chg.Status, spawnTime, readyTime, chg.Summary) + } + + w.Flush() + fmt.Fprintln(Stdout) + + return nil +} + +func (c *cmdTasks) Execute([]string) error { + cli := Client() + id, err := c.GetChangeID(cli) + if err != nil { + return err + } + + return showChange(cli, id) +} + +func showChange(cli *client.Client, chid string) error { + chg, err := cli.Change(chid) + if err != nil { + return err + } + + w := tabWriter() + + fmt.Fprintf(w, i18n.G("Status\tSpawn\tReady\tSummary\n")) + for _, t := range chg.Tasks { + spawnTime := t.SpawnTime.UTC().Format(time.RFC3339) + readyTime := t.ReadyTime.UTC().Format(time.RFC3339) + if t.ReadyTime.IsZero() { + readyTime = "-" + } + summary := t.Summary + if t.Status == "Doing" && t.Progress.Total > 1 { + summary = fmt.Sprintf("%s (%.2f%%)", summary, float64(t.Progress.Done)/float64(t.Progress.Total)*100.0) + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Status, spawnTime, readyTime, summary) + } + + w.Flush() + + for _, t := range chg.Tasks { + if len(t.Log) == 0 { + continue + } + fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, line) + fmt.Fprintln(Stdout, t.Summary) + fmt.Fprintln(Stdout) + for _, line := range t.Log { + fmt.Fprintln(Stdout, line) + } + } + + fmt.Fprintln(Stdout) + + return nil +} + +const line = "......................................................................" diff --git a/cmd/snap/cmd_changes_test.go b/cmd/snap/cmd_changes_test.go new file mode 100644 index 00000000..4744654e --- /dev/null +++ b/cmd/snap/cmd_changes_test.go @@ -0,0 +1,180 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +var mockChangeJSON = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] +}}` + +func (s *SnapSuite) TestChangeSimple(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if n < 2 { + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, mockChangeJSON) + } else { + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + expectedChange := `(?ms)Status +Spawn +Ready +Summary +Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary +` + rest, err := snap.Parser().ParseArgs([]string{"change", "42"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, expectedChange) + c.Check(s.Stderr(), check.Equals, "") + + rest, err = snap.Parser().ParseArgs([]string{"tasks", "42"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, expectedChange) + c.Check(s.Stderr(), check.Equals, "") +} + +var mockChangesJSON = `{"type": "sync", "result": [ + { + "id": "four", + "kind": "install-snap", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2015-02-21T01:02:03Z", + "ready-time": "2015-02-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2015-02-21T01:02:03Z", "ready-time": "2015-02-21T01:02:04Z"}] + }, + { + "id": "one", + "kind": "remove-snap", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-03-21T01:02:03Z", + "ready-time": "2016-03-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-03-21T01:02:03Z", "ready-time": "2016-03-21T01:02:04Z"}] + }, + { + "id": "two", + "kind": "install-snap", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] + }, + { + "id": "three", + "kind": "install-snap", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-01-21T01:02:03Z", + "ready-time": "2016-01-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Do", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-01-21T01:02:03Z", "ready-time": "2016-01-21T01:02:04Z"}] + } +]}` + +func (s *SnapSuite) TestTasksLast(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "GET") + if r.URL.Path == "/v2/changes" { + fmt.Fprintln(w, mockChangesJSON) + return + } + c.Assert(r.URL.Path, check.Equals, "/v2/changes/two") + fmt.Fprintln(w, mockChangeJSON) + }) + expectedChange := `(?ms)Status +Spawn +Ready +Summary +Do +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary +` + rest, err := snap.Parser().ParseArgs([]string{"tasks", "--last=install"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, expectedChange) + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser().ParseArgs([]string{"tasks", "--last=foobar"}) + c.Assert(err, check.NotNil) + c.Assert(err, check.ErrorMatches, `no changes of type "foobar" found`) +} + +func (s *SnapSuite) TestTasksSyntaxError(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"tasks", "--last=install", "42"}) + c.Assert(err, check.NotNil) + c.Assert(err, check.ErrorMatches, `cannot use change ID and type together`) + + _, err = snap.Parser().ParseArgs([]string{"tasks"}) + c.Assert(err, check.NotNil) + c.Assert(err, check.ErrorMatches, `please provide change ID or type with --last=`) +} + +var mockChangeInProgressJSON = `{"type": "sync", "result": { + "id": "uno", + "kind": "foo", + "summary": "...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"kind": "bar", "summary": "some summary", "status": "Doing", "progress": {"done": 50, "total": 100}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] +}}` + +func (s *SnapSuite) TestChangeProgress(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, mockChangeInProgressJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"change", "42"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?ms)Status +Spawn +Ready +Summary +Doing +2016-04-21T01:02:03Z +2016-04-21T01:02:04Z +some summary \(50.00%\) +`) + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_confinement.go b/cmd/snap/cmd_confinement.go new file mode 100644 index 00000000..72f6062e --- /dev/null +++ b/cmd/snap/cmd_confinement.go @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" + + "fmt" + + "github.com/jessevdk/go-flags" +) + +var shortConfinementHelp = i18n.G("Prints the confinement mode the system operates in") +var longConfinementHelp = i18n.G(` +The confinement command will print the confinement mode (strict, partial or none) +the system operates in. +`) + +type cmdConfinement struct{} + +func init() { + addDebugCommand("confinement", shortConfinementHelp, longConfinementHelp, func() flags.Commander { return &cmdConfinement{} }) +} + +func (cmd cmdConfinement) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + sysInfo, err := cli.SysInfo() + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", sysInfo.Confinement) + return nil +} diff --git a/cmd/snap/cmd_confinement_test.go b/cmd/snap/cmd_confinement_test.go new file mode 100644 index 00000000..6237a270 --- /dev/null +++ b/cmd/snap/cmd_confinement_test.go @@ -0,0 +1,39 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConfinement(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"confinement": "strict"}}`) + }) + _, err := snap.Parser().ParseArgs([]string{"debug", "confinement"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "strict\n") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_connect.go b/cmd/snap/cmd_connect.go new file mode 100644 index 00000000..3e401df7 --- /dev/null +++ b/cmd/snap/cmd_connect.go @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdConnect struct { + Positionals struct { + PlugSpec connectPlugSpec `required:"yes"` + SlotSpec connectSlotSpec + } `positional-args:"true"` +} + +var shortConnectHelp = i18n.G("Connects a plug to a slot") +var longConnectHelp = i18n.G(` +The connect command connects a plug to a slot. +It may be called in the following ways: + +$ snap connect : : + +Connects the provided plug to the given slot. + +$ snap connect : + +Connects the specific plug to the only slot in the provided snap that matches +the connected interface. If more than one potential slot exists, the command +fails. + +$ snap connect : + +Connects the provided plug to the slot in the core snap with a name matching +the plug name. +`) + +func init() { + addCommand("connect", shortConnectHelp, longConnectHelp, func() flags.Commander { + return &cmdConnect{} + }, nil, []argDesc{ + // TRANSLATORS: This needs to be wrapped in <>s. + {name: i18n.G(":")}, + // TRANSLATORS: This needs to be wrapped in <>s. + {name: i18n.G(":")}, + }) +} + +func (x *cmdConnect) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // snap connect [:] + if x.Positionals.PlugSpec.Snap != "" && x.Positionals.PlugSpec.Name == "" { + // Move the value of .Snap to .Name and keep .Snap empty + x.Positionals.PlugSpec.Name = x.Positionals.PlugSpec.Snap + x.Positionals.PlugSpec.Snap = "" + } + + cli := Client() + id, err := cli.Connect(x.Positionals.PlugSpec.Snap, x.Positionals.PlugSpec.Name, x.Positionals.SlotSpec.Snap, x.Positionals.SlotSpec.Name) + if err != nil { + return err + } + + _, err = wait(cli, id) + return err +} diff --git a/cmd/snap/cmd_connect_test.go b/cmd/snap/cmd_connect_test.go new file mode 100644 index 00000000..463dfb14 --- /dev/null +++ b/cmd/snap/cmd_connect_test.go @@ -0,0 +1,329 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "os" + + "github.com/jessevdk/go-flags" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConnectHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] connect [:] [:] + +The connect command connects a plug to a slot. +It may be called in the following ways: + +$ snap connect : : + +Connects the provided plug to the given slot. + +$ snap connect : + +Connects the specific plug to the only slot in the provided snap that matches +the connected interface. If more than one potential slot exists, the command +fails. + +$ snap connect : + +Connects the provided plug to the slot in the core snap with a name matching +the plug name. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"connect", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestConnectExplicitEverything(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"connect", "producer:plug", "consumer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestConnectExplicitPlugImplicitSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"connect", "producer:plug", "consumer"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestConnectImplicitPlugExplicitSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"connect", "plug", "consumer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestConnectImplicitPlugImplicitSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "connect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"connect", "plug", "consumer"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) +} + +var fortestingConnectionList = client.Connections{ + Slots: []client.Slot{ + { + Snap: "core", + Name: "x11", + Interface: "x11", + }, + { + Snap: "core", + Name: "core-support", + Interface: "core-support", + Connections: []client.PlugRef{ + { + Snap: "core", + Name: "core-support-plug", + }, + }, + }, + { + Snap: "wake-up-alarm", + Name: "toggle", + Interface: "bool-file", + Label: "Alarm toggle", + }, + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.PlugRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }, + Plugs: []client.Plug{ + { + Snap: "core", + Name: "core-support-plug", + Interface: "core-support", + Connections: []client.SlotRef{ + { + Snap: "core", + Name: "core-support", + }, + }, + }, + { + Snap: "core", + Name: "network-bind-plug", + Interface: "network-bind", + }, + { + Snap: "paste-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + }, + { + Snap: "potato", + Name: "frying", + Interface: "frying", + Label: "Ability to fry a network service", + }, + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "bool-file", + Label: "Capslock indicator LED", + Connections: []client.SlotRef{ + { + Snap: "canonical-pi2", + Name: "pin-13", + }, + }, + }, + }, +} + +func (s *SnapSuite) TestConnectCompletion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Assert(r.Method, Equals, "GET") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": fortestingConnectionList, + }) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + os.Setenv("GO_FLAGS_COMPLETION", "verbose") + defer os.Unsetenv("GO_FLAGS_COMPLETION") + + expected := []flags.Completion{} + parser := Parser() + parser.CompletionHandler = func(obtained []flags.Completion) { + c.Check(obtained, DeepEquals, expected) + } + + expected = []flags.Completion{{Item: "core:"}, {Item: "paste-daemon:"}, {Item: "potato:"}} + _, err := parser.ParseArgs([]string{"connect", ""}) + c.Assert(err, IsNil) + + // connect's first argument can't start with : (only for the 2nd arg, the slot) + expected = nil + _, err = parser.ParseArgs([]string{"connect", ":"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "paste-daemon:network-listening", Description: "plug"}} + _, err = parser.ParseArgs([]string{"connect", "pa"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "core:"}, {Item: "wake-up-alarm:"}} + _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "wake-up-alarm:toggle", Description: "slot"}} + _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", "w"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: ":x11", Description: "slot"}} + _, err = parser.ParseArgs([]string{"connect", "paste-daemon:network-listening", ":"}) + c.Assert(err, IsNil) + + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_create_key.go b/cmd/snap/cmd_create_key.go new file mode 100644 index 00000000..26e03e42 --- /dev/null +++ b/cmd/snap/cmd_create_key.go @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + + "github.com/jessevdk/go-flags" + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +type cmdCreateKey struct { + Positional struct { + KeyName string + } `positional-args:"true"` +} + +func init() { + cmd := addCommand("create-key", + i18n.G("Create cryptographic key pair"), + i18n.G("Create a cryptographic key pair that can be used for signing assertions."), + func() flags.Commander { + return &cmdCreateKey{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Name of key to create; defaults to 'default'"), + }}) + cmd.hidden = true +} + +func (x *cmdCreateKey) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + keyName := x.Positional.KeyName + if keyName == "" { + keyName = "default" + } + if !asserts.IsValidAccountKeyName(keyName) { + return fmt.Errorf(i18n.G("key name %q is not valid; only ASCII letters, digits, and hyphens are allowed"), keyName) + } + + fmt.Fprint(Stdout, i18n.G("Passphrase: ")) + passphrase, err := terminal.ReadPassword(0) + fmt.Fprint(Stdout, "\n") + if err != nil { + return err + } + fmt.Fprint(Stdout, i18n.G("Confirm passphrase: ")) + confirmPassphrase, err := terminal.ReadPassword(0) + fmt.Fprint(Stdout, "\n") + if err != nil { + return err + } + if string(passphrase) != string(confirmPassphrase) { + return errors.New("passphrases do not match") + } + if err != nil { + return err + } + + manager := asserts.NewGPGKeypairManager() + return manager.Generate(string(passphrase), keyName) +} diff --git a/cmd/snap/cmd_create_key_test.go b/cmd/snap/cmd_create_key_test.go new file mode 100644 index 00000000..42089097 --- /dev/null +++ b/cmd/snap/cmd_create_key_test.go @@ -0,0 +1,34 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestCreateKeyInvalidCharacters(c *C) { + _, err := snap.Parser().ParseArgs([]string{"create-key", "a b"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "key name \"a b\" is not valid; only ASCII letters, digits, and hyphens are allowed") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_create_user.go b/cmd/snap/cmd_create_user.go new file mode 100644 index 00000000..eff24087 --- /dev/null +++ b/cmd/snap/cmd_create_user.go @@ -0,0 +1,115 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortCreateUserHelp = i18n.G("Creates a local system user") +var longCreateUserHelp = i18n.G(` +The create-user command creates a local system user with the username and SSH +keys registered on the store account identified by the provided email address. + +An account can be setup at https://login.ubuntu.com. +`) + +type cmdCreateUser struct { + Positional struct { + Email string + } `positional-args:"yes"` + + JSON bool `long:"json"` + Sudoer bool `long:"sudoer"` + Known bool `long:"known"` + ForceManaged bool `long:"force-managed"` +} + +func init() { + cmd := addCommand("create-user", shortCreateUserHelp, longCreateUserHelp, func() flags.Commander { return &cmdCreateUser{} }, + map[string]string{ + "json": i18n.G("Output results in JSON format"), + "sudoer": i18n.G("Grant sudo access to the created user"), + "known": i18n.G("Use known assertions for user creation"), + "force-managed": i18n.G("Force adding the user, even if the device is already managed"), + }, []argDesc{{ + // TRANSLATORS: This is a noun, and it needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. Also, note users on login.ubuntu.com can have multiple email addresses. + desc: i18n.G("An email of a user on login.ubuntu.com"), + }}) + cmd.hidden = true +} + +func (x *cmdCreateUser) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + + options := client.CreateUserOptions{ + Email: x.Positional.Email, + Sudoer: x.Sudoer, + Known: x.Known, + ForceManaged: x.ForceManaged, + } + + var results []*client.CreateUserResult + var result *client.CreateUserResult + var err error + + if options.Email == "" && options.Known { + results, err = cli.CreateUsers([]*client.CreateUserOptions{&options}) + } else { + result, err = cli.CreateUser(&options) + if err == nil { + results = append(results, result) + } + } + + createErr := err + + // Print results regardless of error because some users may have been created. + if x.JSON { + var data []byte + if result != nil { + data, err = json.Marshal(result) + } else if len(results) > 0 { + data, err = json.Marshal(results) + } + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", data) + } else { + for _, result := range results { + fmt.Fprintf(Stdout, i18n.G("created user %q\n"), result.Username) + } + } + + return createErr +} diff --git a/cmd/snap/cmd_create_user_test.go b/cmd/snap/cmd_create_user_test.go new file mode 100644 index 00000000..50726570 --- /dev/null +++ b/cmd/snap/cmd_create_user_test.go @@ -0,0 +1,150 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "net/http" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +func makeCreateUserChecker(c *check.C, n *int, email string, sudoer, known bool) func(w http.ResponseWriter, r *http.Request) { + f := func(w http.ResponseWriter, r *http.Request) { + switch *n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/create-user") + var gotBody map[string]interface{} + dec := json.NewDecoder(r.Body) + err := dec.Decode(&gotBody) + c.Assert(err, check.IsNil) + + wantBody := make(map[string]interface{}) + if email != "" { + wantBody["email"] = "one@email.com" + } + if sudoer { + wantBody["sudoer"] = true + } + if known { + wantBody["known"] = true + } + c.Check(gotBody, check.DeepEquals, wantBody) + + if email == "" { + fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "karl", "ssh-keys": ["a","b"]}]}`) + } else { + fmt.Fprintln(w, `{"type": "sync", "result": {"username": "karl", "ssh-keys": ["a","b"]}}`) + } + default: + c.Fatalf("got too many requests (now on %d)", *n+1) + } + + *n++ + } + return f +} + +func (s *SnapSuite) TestCreateUserNoSudoer(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false)) + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "one@email.com"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + c.Assert(s.Stdout(), check.Equals, `created user "karl"`+"\n") + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestCreateUserSudoer(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", true, false)) + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--sudoer", "one@email.com"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + c.Assert(s.Stdout(), check.Equals, `created user "karl"`+"\n") + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestCreateUserJSON(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, false)) + + expectedResponse := &client.CreateUserResult{ + Username: "karl", + SSHKeys: []string{"a", "b"}, + } + actualResponse := &client.CreateUserResult{} + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--json", "one@email.com"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + json.Unmarshal(s.stdout.Bytes(), actualResponse) + c.Assert(actualResponse, check.DeepEquals, expectedResponse) + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestCreateUserNoEmailJSON(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true)) + + var expectedResponse = []*client.CreateUserResult{{ + Username: "karl", + SSHKeys: []string{"a", "b"}, + }} + var actualResponse []*client.CreateUserResult + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--json", "--known"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + json.Unmarshal(s.stdout.Bytes(), &actualResponse) + c.Assert(actualResponse, check.DeepEquals, expectedResponse) + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestCreateUserKnown(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "one@email.com", false, true)) + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--known", "one@email.com"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestCreateUserKnownNoEmail(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeCreateUserChecker(c, &n, "", false, true)) + + rest, err := snap.Parser().ParseArgs([]string{"create-user", "--known"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) +} diff --git a/cmd/snap/cmd_debug.go b/cmd/snap/cmd_debug.go new file mode 100644 index 00000000..5a9351f4 --- /dev/null +++ b/cmd/snap/cmd_debug.go @@ -0,0 +1,34 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" +) + +type cmdDebug struct{} + +var shortDebugHelp = i18n.G("Runs debug commands") +var longDebugHelp = i18n.G(` +The debug command contains a selection of additional sub-commands. + +Debug commands can be removed without notice and may not work on +non-development systems. +`) diff --git a/cmd/snap/cmd_delete_key.go b/cmd/snap/cmd_delete_key.go new file mode 100644 index 00000000..1002a775 --- /dev/null +++ b/cmd/snap/cmd_delete_key.go @@ -0,0 +1,57 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +type cmdDeleteKey struct { + Positional struct { + KeyName keyName + } `positional-args:"true" required:"true"` +} + +func init() { + cmd := addCommand("delete-key", + i18n.G("Delete cryptographic key pair"), + i18n.G("Delete the local cryptographic key pair with the given name."), + func() flags.Commander { + return &cmdDeleteKey{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Name of key to delete"), + }}) + cmd.hidden = true +} + +func (x *cmdDeleteKey) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + manager := asserts.NewGPGKeypairManager() + return manager.Delete(string(x.Positional.KeyName)) +} diff --git a/cmd/snap/cmd_delete_key_test.go b/cmd/snap/cmd_delete_key_test.go new file mode 100644 index 00000000..65b83dc3 --- /dev/null +++ b/cmd/snap/cmd_delete_key_test.go @@ -0,0 +1,64 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "encoding/json" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapKeysSuite) TestDeleteKeyRequiresName(c *C) { + _, err := snap.Parser().ParseArgs([]string{"delete-key"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "the required argument `` was not provided") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestDeleteKeyNonexistent(c *C) { + _, err := snap.Parser().ParseArgs([]string{"delete-key", "nonexistent"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestDeleteKey(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"delete-key", "another"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") + _, err = snap.Parser().ParseArgs([]string{"keys", "--json"}) + c.Assert(err, IsNil) + expectedResponse := []snap.Key{ + { + Name: "default", + Sha3_384: "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ", + }, + } + var obtainedResponse []snap.Key + json.Unmarshal(s.stdout.Bytes(), &obtainedResponse) + c.Check(obtainedResponse, DeepEquals, expectedResponse) + c.Check(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_disconnect.go b/cmd/snap/cmd_disconnect.go new file mode 100644 index 00000000..cc524efb --- /dev/null +++ b/cmd/snap/cmd_disconnect.go @@ -0,0 +1,89 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdDisconnect struct { + Positionals struct { + Offer disconnectSlotOrPlugSpec `required:"true"` + Use disconnectSlotSpec + } `positional-args:"true"` +} + +var shortDisconnectHelp = i18n.G("Disconnects a plug from a slot") +var longDisconnectHelp = i18n.G(` +The disconnect command disconnects a plug from a slot. +It may be called in the following ways: + +$ snap disconnect : : + +Disconnects the specific plug from the specific slot. + +$ snap disconnect : + +Disconnects everything from the provided plug or slot. +The snap name may be omitted for the core snap. +`) + +func init() { + addCommand("disconnect", shortDisconnectHelp, longDisconnectHelp, func() flags.Commander { + return &cmdDisconnect{} + }, nil, []argDesc{ + // TRANSLATORS: This needs to be wrapped in <>s. + {name: i18n.G(":")}, + // TRANSLATORS: This needs to be wrapped in <>s. + {name: i18n.G(":")}, + }) +} + +func (x *cmdDisconnect) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + offer := x.Positionals.Offer.SnapAndName + use := x.Positionals.Use.SnapAndName + + // snap disconnect : + // snap disconnect + if use.Snap == "" && use.Name == "" { + // Swap Offer and Use around + offer, use = use, offer + } + if use.Name == "" { + return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", use.Snap) + } + + cli := Client() + id, err := cli.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name) + if err != nil { + return err + } + + _, err = wait(cli, id) + return err +} diff --git a/cmd/snap/cmd_disconnect_test.go b/cmd/snap/cmd_disconnect_test.go new file mode 100644 index 00000000..31ffe1ff --- /dev/null +++ b/cmd/snap/cmd_disconnect_test.go @@ -0,0 +1,228 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "os" + + "github.com/jessevdk/go-flags" + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestDisconnectHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] disconnect [:] [:] + +The disconnect command disconnects a plug from a slot. +It may be called in the following ways: + +$ snap disconnect : : + +Disconnects the specific plug from the specific slot. + +$ snap disconnect : + +Disconnects everything from the provided plug or slot. +The snap name may be omitted for the core snap. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"disconnect", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestDisconnectExplicitEverything(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"disconnect", "producer:plug", "consumer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDisconnectEverythingFromSpecificSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "", + "plug": "", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"disconnect", "consumer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDisconnectEverythingFromSpecificSnapPlugOrSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "", + "plug": "", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "plug-or-slot", + }, + }, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"disconnect", "consumer:plug-or-slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDisconnectEverythingFromSpecificSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Fatalf("expected nothing to reach the server") + }) + rest, err := Parser().ParseArgs([]string{"disconnect", "consumer"}) + c.Assert(err, ErrorMatches, `please provide the plug or slot name to disconnect from snap "consumer"`) + c.Assert(rest, DeepEquals, []string{"consumer"}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDisconnectCompletion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Assert(r.Method, Equals, "GET") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": fortestingConnectionList, + }) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + os.Setenv("GO_FLAGS_COMPLETION", "verbose") + defer os.Unsetenv("GO_FLAGS_COMPLETION") + + expected := []flags.Completion{} + parser := Parser() + parser.CompletionHandler = func(obtained []flags.Completion) { + c.Check(obtained, DeepEquals, expected) + } + + expected = []flags.Completion{{Item: "canonical-pi2:"}, {Item: "core:"}, {Item: "keyboard-lights:"}} + _, err := parser.ParseArgs([]string{"disconnect", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "canonical-pi2:pin-13", Description: "slot"}} + _, err = parser.ParseArgs([]string{"disconnect", "ca"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: ":core-support", Description: "slot"}, {Item: ":core-support-plug", Description: "plug"}} + _, err = parser.ParseArgs([]string{"disconnect", ":"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "keyboard-lights:capslock-led", Description: "plug"}} + _, err = parser.ParseArgs([]string{"disconnect", "k"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "canonical-pi2:"}, {Item: "core:"}} + _, err = parser.ParseArgs([]string{"disconnect", "keyboard-lights:capslock-led", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "canonical-pi2:pin-13", Description: "slot"}} + _, err = parser.ParseArgs([]string{"disconnect", "keyboard-lights:capslock-led", "ca"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: ":core-support", Description: "slot"}} + _, err = parser.ParseArgs([]string{"disconnect", ":core-support-plug", ":"}) + c.Assert(err, IsNil) + + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_download.go b/cmd/snap/cmd_download.go new file mode 100644 index 00000000..a3283a25 --- /dev/null +++ b/cmd/snap/cmd_download.go @@ -0,0 +1,134 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/image" + "github.com/snapcore/snapd/snap" +) + +type cmdDownload struct { + channelMixin + Revision string `long:"revision"` + + Positional struct { + Snap remoteSnapName + } `positional-args:"true" required:"true"` +} + +var shortDownloadHelp = i18n.G("Downloads the given snap") +var longDownloadHelp = i18n.G(` +The download command downloads the given snap and its supporting assertions +to the current directory under .snap and .assert file extensions, respectively. +`) + +func init() { + addCommand("download", shortDownloadHelp, longDownloadHelp, func() flags.Commander { + return &cmdDownload{} + }, channelDescs.also(map[string]string{ + "revision": i18n.G("Download the given revision of a snap, to which you must have developer access"), + }), []argDesc{{ + name: "", + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Snap name"), + }}) +} + +func fetchSnapAssertions(tsto *image.ToolingStore, snapPath string, snapInfo *snap.Info) error { + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: sysdb.Trusted(), + }) + if err != nil { + return err + } + + assertPath := strings.TrimSuffix(snapPath, filepath.Ext(snapPath)) + ".assert" + w, err := os.Create(assertPath) + if err != nil { + return fmt.Errorf(i18n.G("cannot create assertions file: %v"), err) + } + defer w.Close() + + encoder := asserts.NewEncoder(w) + save := func(a asserts.Assertion) error { + return encoder.Encode(a) + } + f := tsto.AssertionFetcher(db, save) + + _, err = image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db) + return err +} + +func (x *cmdDownload) Execute(args []string) error { + if err := x.setChannelFromCommandline(); err != nil { + return err + } + + if len(args) > 0 { + return ErrExtraArgs + } + + var revision snap.Revision + if x.Revision == "" { + revision = snap.R(0) + } else { + var err error + revision, err = snap.ParseRevision(x.Revision) + if err != nil { + return err + } + } + + snapName := string(x.Positional.Snap) + + tsto, err := image.NewToolingStore() + if err != nil { + return err + } + + fmt.Fprintf(Stderr, i18n.G("Fetching snap %q\n"), snapName) + dlOpts := image.DownloadOptions{ + TargetDir: "", // cwd + Channel: x.Channel, + } + snapPath, snapInfo, err := tsto.DownloadSnap(snapName, revision, &dlOpts) + if err != nil { + return err + } + + fmt.Fprintf(Stderr, i18n.G("Fetching assertions for %q\n"), snapName) + err = fetchSnapAssertions(tsto, snapPath, snapInfo) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/snap/cmd_ensure_state_soon.go b/cmd/snap/cmd_ensure_state_soon.go new file mode 100644 index 00000000..d3077058 --- /dev/null +++ b/cmd/snap/cmd_ensure_state_soon.go @@ -0,0 +1,44 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" +) + +type cmdEnsureStateSoon struct{} + +func init() { + cmd := addDebugCommand("ensure-state-soon", + "(internal) trigger an ensure runn in the state engine", + "(internal) trigger an ensure runn in the state engine", + func() flags.Commander { + return &cmdEnsureStateSoon{} + }) + cmd.hidden = true +} + +func (x *cmdEnsureStateSoon) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return Client().Debug("ensure-state-soon", nil, nil) +} diff --git a/cmd/snap/cmd_ensure_state_soon_test.go b/cmd/snap/cmd_ensure_state_soon_test.go new file mode 100644 index 00000000..d2465b3d --- /dev/null +++ b/cmd/snap/cmd_ensure_state_soon_test.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestEnsureStateSoon(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"ensure-state-soon"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": true}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"debug", "ensure-state-soon"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_export_key.go b/cmd/snap/cmd_export_key.go new file mode 100644 index 00000000..9016c006 --- /dev/null +++ b/cmd/snap/cmd_export_key.go @@ -0,0 +1,97 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +type cmdExportKey struct { + Account string `long:"account"` + Positional struct { + KeyName keyName + } `positional-args:"true"` +} + +func init() { + cmd := addCommand("export-key", + i18n.G("Export cryptographic public key"), + i18n.G("Export a public key assertion body that may be imported by other systems."), + func() flags.Commander { + return &cmdExportKey{} + }, map[string]string{ + "account": i18n.G("Format public key material as a request for an account-key for this account-id"), + }, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Name of key to export"), + }}) + cmd.hidden = true +} + +func (x *cmdExportKey) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + keyName := string(x.Positional.KeyName) + if keyName == "" { + keyName = "default" + } + + manager := asserts.NewGPGKeypairManager() + if x.Account != "" { + privKey, err := manager.GetByName(keyName) + if err != nil { + return err + } + pubKey := privKey.PublicKey() + headers := map[string]interface{}{ + "account-id": x.Account, + "name": keyName, + "public-key-sha3-384": pubKey.ID(), + "since": time.Now().UTC().Format(time.RFC3339), + // XXX: To support revocation, we need to check for matching known assertions and set a suitable revision if we find one. + } + body, err := asserts.EncodePublicKey(pubKey) + if err != nil { + return err + } + assertion, err := asserts.SignWithoutAuthority(asserts.AccountKeyRequestType, headers, body, privKey) + if err != nil { + return err + } + fmt.Fprint(Stdout, string(asserts.Encode(assertion))) + } else { + encoded, err := manager.Export(keyName) + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", encoded) + } + return nil +} diff --git a/cmd/snap/cmd_export_key_test.go b/cmd/snap/cmd_export_key_test.go new file mode 100644 index 00000000..21e65963 --- /dev/null +++ b/cmd/snap/cmd_export_key_test.go @@ -0,0 +1,84 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapKeysSuite) TestExportKeyNonexistent(c *C) { + _, err := snap.Parser().ParseArgs([]string{"export-key", "nonexistent"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "cannot find key named \"nonexistent\" in GPG keyring") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestExportKeyDefault(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"export-key"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(pubKey.ID(), Equals, "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestExportKeyNonDefault(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"export-key", "another"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + pubKey, err := asserts.DecodePublicKey(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(pubKey.ID(), Equals, "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestExportKeyAccount(c *C) { + storeSigning := assertstest.NewStoreStack("canonical", nil) + manager := asserts.NewGPGKeypairManager() + assertstest.NewAccount(storeSigning, "developer1", nil, "") + rest, err := snap.Parser().ParseArgs([]string{"export-key", "another", "--account=developer1"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + assertion, err := asserts.Decode(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(assertion.Type(), Equals, asserts.AccountKeyRequestType) + c.Check(assertion.Revision(), Equals, 0) + c.Check(assertion.HeaderString("account-id"), Equals, "developer1") + c.Check(assertion.HeaderString("name"), Equals, "another") + c.Check(assertion.HeaderString("public-key-sha3-384"), Equals, "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L") + since, err := time.Parse(time.RFC3339, assertion.HeaderString("since")) + c.Assert(err, IsNil) + zone, offset := since.Zone() + c.Check(zone, Equals, "UTC") + c.Check(offset, Equals, 0) + c.Check(s.Stderr(), Equals, "") + privKey, err := manager.Get(assertion.HeaderString("public-key-sha3-384")) + c.Assert(err, IsNil) + err = asserts.SignatureCheck(assertion, privKey.PublicKey()) + c.Assert(err, IsNil) +} diff --git a/cmd/snap/cmd_find.go b/cmd/snap/cmd_find.go new file mode 100644 index 00000000..bf68c428 --- /dev/null +++ b/cmd/snap/cmd_find.go @@ -0,0 +1,151 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" +) + +var shortFindHelp = i18n.G("Finds packages to install") +var longFindHelp = i18n.G(` +The find command queries the store for available packages in the stable channel. +`) + +func getPrice(prices map[string]float64, currency string) (float64, string, error) { + // If there are no prices, then the snap is free + if len(prices) == 0 { + // TRANSLATORS: free as in gratis + return 0, "", errors.New(i18n.G("snap is free")) + } + + // Look up the price by currency code + val, ok := prices[currency] + + // Fall back to dollars + if !ok { + currency = "USD" + val, ok = prices["USD"] + } + + // If there aren't even dollars, grab the first currency, + // ordered alphabetically by currency code + if !ok { + currency = "ZZZ" + for c, v := range prices { + if c < currency { + currency, val = c, v + } + } + } + + return val, currency, nil +} + +type SectionName string + +func (s SectionName) Complete(match string) []flags.Completion { + if ret, err := completeFromSortedFile(dirs.SnapSectionsFile, match); err == nil { + return ret + } + + cli := Client() + sections, err := cli.Sections() + if err != nil { + return nil + } + ret := make([]flags.Completion, 0, len(sections)) + for _, s := range sections { + if strings.HasPrefix(s, match) { + ret = append(ret, flags.Completion{Item: s}) + } + } + return ret +} + +type cmdFind struct { + Private bool `long:"private"` + Section SectionName `long:"section"` + Positional struct { + Query string + } `positional-args:"yes"` +} + +func init() { + addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander { + return &cmdFind{} + }, map[string]string{ + "private": i18n.G("Search private snaps"), + "section": i18n.G("Restrict the search to a given section"), + }, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + }}).alias = "search" +} + +func (x *cmdFind) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // magic! `snap find` returns the featured snaps + if x.Positional.Query == "" && x.Section == "" { + x.Section = "featured" + } + + return findSnaps(&client.FindOptions{ + Private: x.Private, + Section: string(x.Section), + Query: x.Positional.Query, + }) +} + +func findSnaps(opts *client.FindOptions) error { + cli := Client() + snaps, resInfo, err := cli.Find(opts) + if err != nil { + return err + } + + if len(snaps) == 0 { + // TRANSLATORS: the %q is the (quoted) query the user entered + fmt.Fprintf(Stderr, i18n.G("The search %q returned 0 snaps\n"), opts.Query) + return nil + } + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Name\tVersion\tDeveloper\tNotes\tSummary")) + + for _, snap := range snaps { + // TODO: get snap.Publisher, so we can only show snap.Developer if it's different + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Developer, NotesFromRemote(snap, resInfo), snap.Summary) + } + + return nil +} diff --git a/cmd/snap/cmd_find_test.go b/cmd/snap/cmd_find_test.go new file mode 100644 index 00000000..561d5ff4 --- /dev/null +++ b/cmd/snap/cmd_find_test.go @@ -0,0 +1,364 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + "github.com/jessevdk/go-flags" + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +const findJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + }, + { + "channel": "stable", + "confinement": "strict", + "description": "This is a simple hello world example.", + "developer": "canonical", + "download-size": 20480, + "icon": "", + "id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "private": false, + "resource": "/v2/snaps/hello-world", + "revision": "26", + "status": "available", + "summary": "Hello world example", + "type": "app", + "version": "6.1" + }, + { + "channel": "stable", + "confinement": "strict", + "description": "1.0GB", + "developer": "noise", + "download-size": 512004096, + "icon": "", + "id": "asXOGCreK66DIAdyXmucwspTMgqA4rne", + "name": "hello-huge", + "private": false, + "resource": "/v2/snaps/hello-huge", + "revision": "1", + "status": "available", + "summary": "a really big snap", + "type": "app", + "version": "1.0" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindSnapName(c *check.C) { + n := 0 + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + if q.Get("q") == "" { + v, ok := q["section"] + c.Check(ok, check.Equals, true) + c.Check(v, check.DeepEquals, []string{""}) + } + fmt.Fprintln(w, findJSON) + default: + c.Fatalf("expected to get 2 requests, now on %d", n+1) + } + n++ + }) + + rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + + c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary +hello +2.10 +canonical +- +GNU Hello, the "hello world" snap +hello-world +6.1 +canonical +- +Hello world example +hello-huge +1.0 +noise +- +a really big snap +`) + c.Check(s.Stderr(), check.Equals, "") + + s.ResetStdStreams() +} + +const findHelloJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + }, + { + "channel": "stable", + "confinement": "strict", + "description": "1.0GB", + "developer": "noise", + "download-size": 512004096, + "icon": "", + "id": "asXOGCreK66DIAdyXmucwspTMgqA4rne", + "name": "hello-huge", + "private": false, + "resource": "/v2/snaps/hello-huge", + "revision": "1", + "status": "available", + "summary": "a really big snap", + "type": "app", + "version": "1.0" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindHello(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + c.Check(q.Get("q"), check.Equals, "hello") + fmt.Fprintln(w, findHelloJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary +hello +2.10 +canonical +- +GNU Hello, the "hello world" snap +hello-huge +1.0 +noise +- +a really big snap +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const findPricedJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "prices": {"GBP": 1.99, "USD": 2.99}, + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "priced", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindPriced(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, findPricedJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary +hello +2.10 +canonical +1.99GBP +GNU Hello, the "hello world" snap +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const findPricedAndBoughtJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "prices": {"GBP": 1.99, "USD": 2.99}, + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindPricedAndBought(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, findPricedAndBoughtJSON) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"find", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Developer +Notes +Summary +hello +2.10 +canonical +bought +GNU Hello, the "hello world" snap +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestFindNothingMeansFeaturedSection(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + c.Check(r.URL.Query().Get("section"), check.Equals, "featured") + fmt.Fprintln(w, findJSON) + default: + c.Fatalf("expected to get 1 request, now on %d", n+1) + } + n++ + }) + + _, err := snap.Parser().ParseArgs([]string{"find"}) + c.Assert(err, check.IsNil) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestSectionCompletion(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0, 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/sections") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []string{"foo", "bar", "baz"}, + }) + default: + c.Fatalf("expected to get 2 requests, now on #%d", n+1) + } + n++ + }) + + c.Check(snap.SectionName("").Complete(""), check.DeepEquals, []flags.Completion{ + {Item: "foo"}, + {Item: "bar"}, + {Item: "baz"}, + }) + + c.Check(snap.SectionName("").Complete("f"), check.DeepEquals, []flags.Completion{ + {Item: "foo"}, + }) +} diff --git a/cmd/snap/cmd_first_boot.go b/cmd/snap/cmd_first_boot.go new file mode 100644 index 00000000..46c2ed98 --- /dev/null +++ b/cmd/snap/cmd_first_boot.go @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdInternalFirstBoot struct{} + +func init() { + cmd := addCommand("firstboot", + "internal", + "internal", func() flags.Commander { + return &cmdInternalFirstBoot{} + }, nil, nil) + cmd.hidden = true +} + +// WARNING: do not remove this command, older systems may still have +// a systemd snapd.firstboot.service job in /etc/systemd/system +// that we did not cleanup. so we need this dummy command or +// those units will start failing. +func (x *cmdInternalFirstBoot) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + fmt.Fprintf(Stderr, "firstboot command is deprecated\n") + return nil +} diff --git a/cmd/snap/cmd_get.go b/cmd/snap/cmd_get.go new file mode 100644 index 00000000..e7b0d64c --- /dev/null +++ b/cmd/snap/cmd_get.go @@ -0,0 +1,262 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + "os" + "sort" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "golang.org/x/crypto/ssh/terminal" +) + +var shortGetHelp = i18n.G("Prints configuration options") +var longGetHelp = i18n.G(` +The get command prints configuration options for the provided snap. + + $ snap get snap-name username + frank + +If multiple option names are provided, a document is returned: + + $ snap get snap-name username password + { + "username": "frank", + "password": "..." + } + +Nested values may be retrieved via a dotted path: + + $ snap get snap-name author.name + frank +`) + +type cmdGet struct { + Positional struct { + Snap installedSnapName `required:"yes"` + Keys []string + } `positional-args:"yes"` + + Typed bool `short:"t"` + Document bool `short:"d"` + List bool `short:"l"` +} + +func init() { + addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} }, + map[string]string{ + "d": i18n.G("Always return document, even with single key"), + "l": i18n.G("Always return list, even with single key"), + "t": i18n.G("Strict typing with nulls and quoted strings"), + }, []argDesc{ + { + name: "", + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("The snap whose conf is being requested"), + }, + { + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Key of interest within the configuration"), + }, + }) +} + +type ConfigValue struct { + Path string + Value interface{} +} + +type byConfigPath []ConfigValue + +func (s byConfigPath) Len() int { return len(s) } +func (s byConfigPath) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byConfigPath) Less(i, j int) bool { + other := s[j].Path + for k, c := range s[i].Path { + if len(other) <= k { + return false + } + + switch { + case c == rune(other[k]): + continue + case c == '.': + return true + case other[k] == '.' || c > rune(other[k]): + return false + } + return true + } + return true +} + +func sortByPath(config []ConfigValue) { + sort.Sort(byConfigPath(config)) +} + +func flattenConfig(cfg map[string]interface{}, root bool) (values []ConfigValue) { + const docstr = "{...}" + for k, v := range cfg { + if input, ok := v.(map[string]interface{}); ok { + if root { + values = append(values, ConfigValue{k, docstr}) + } else { + for kk, vv := range input { + p := k + "." + kk + if _, ok := vv.(map[string]interface{}); ok { + values = append(values, ConfigValue{p, docstr}) + } else { + values = append(values, ConfigValue{p, vv}) + } + } + } + } else { + values = append(values, ConfigValue{k, v}) + } + } + sortByPath(values) + return values +} + +func rootRequested(confKeys []string) bool { + return len(confKeys) == 0 +} + +// outputJson will be used when the user requested "document" output via +// the "-d" commandline switch. +func (c *cmdGet) outputJson(conf interface{}) error { + bytes, err := json.MarshalIndent(conf, "", "\t") + if err != nil { + return err + } + + fmt.Fprintln(Stdout, string(bytes)) + return nil +} + +// outputList will be used when the user requested list output via the +// "-l" commandline switch. +func (x *cmdGet) outputList(conf map[string]interface{}) error { + if rootRequested(x.Positional.Keys) && len(conf) == 0 { + return fmt.Errorf("snap %q has no configuration", x.Positional.Snap) + } + + w := tabWriter() + defer w.Flush() + + fmt.Fprintf(w, "Key\tValue\n") + values := flattenConfig(conf, rootRequested(x.Positional.Keys)) + for _, v := range values { + fmt.Fprintf(w, "%s\t%v\n", v.Path, v.Value) + } + return nil +} + +var isTerminal = func() bool { + return terminal.IsTerminal(int(os.Stdin.Fd())) +} + +// outputDefault will be used when no commandline switch to override the +// output where used. The output follows the following rules: +// - a single key with a string value is printed directly +// - multiple keys are printed as a list to the terminal (if there is one) +// or as json if there is no terminal +// - the option "typed" is honored +func (x *cmdGet) outputDefault(conf map[string]interface{}, snapName string, confKeys []string) error { + if rootRequested(confKeys) && len(conf) == 0 { + return fmt.Errorf("snap %q has no configuration", snapName) + } + + var confToPrint interface{} = conf + + if len(confKeys) == 1 { + // if single key was requested, then just output the + // value unless it's a map, in which case it will be + // printed as a list below. + if _, ok := conf[confKeys[0]].(map[string]interface{}); !ok { + confToPrint = conf[confKeys[0]] + } + } + + // conf looks like a map + if cfg, ok := confToPrint.(map[string]interface{}); ok { + if isTerminal() { + return x.outputList(cfg) + } + + // TODO: remove this conditional and the warning below + // after a transition period. + fmt.Fprintf(Stderr, i18n.G(`WARNING: The output of "snap get" will become a list with columns - use -d or -l to force the output format.\n`)) + return x.outputJson(confToPrint) + } + + if s, ok := confToPrint.(string); ok && !x.Typed { + fmt.Fprintln(Stdout, s) + return nil + } + + if confToPrint != nil || x.Typed { + return x.outputJson(confToPrint) + } + + fmt.Fprintln(Stdout, "") + return nil + +} + +func (x *cmdGet) Execute(args []string) error { + if len(args) > 0 { + // TRANSLATORS: the %s is the list of extra arguments + return fmt.Errorf(i18n.G("too many arguments: %s"), strings.Join(args, " ")) + } + + if x.Document && x.Typed { + return fmt.Errorf("cannot use -d and -t together") + } + + if x.Document && x.List { + return fmt.Errorf("cannot use -d and -l together") + } + + snapName := string(x.Positional.Snap) + confKeys := x.Positional.Keys + + cli := Client() + conf, err := cli.Conf(snapName, confKeys) + if err != nil { + return err + } + + switch { + case x.Document: + return x.outputJson(conf) + case x.List: + return x.outputList(conf) + default: + return x.outputDefault(conf, snapName, confKeys) + } +} diff --git a/cmd/snap/cmd_get_base_declaration.go b/cmd/snap/cmd_get_base_declaration.go new file mode 100644 index 00000000..c0795001 --- /dev/null +++ b/cmd/snap/cmd_get_base_declaration.go @@ -0,0 +1,51 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdGetBaseDeclaration struct{} + +func init() { + addDebugCommand("get-base-declaration", + "(internal) obtain the base declaration for all interfaces", + "(internal) obtain the base declaration for all interfaces", + func() flags.Commander { + return &cmdGetBaseDeclaration{} + }) +} + +func (x *cmdGetBaseDeclaration) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + var resp struct { + BaseDeclaration string `json:"base-declaration"` + } + if err := Client().Debug("get-base-declaration", nil, &resp); err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", resp.BaseDeclaration) + return nil +} diff --git a/cmd/snap/cmd_get_base_declaration_test.go b/cmd/snap/cmd_get_base_declaration_test.go new file mode 100644 index 00000000..4b000da3 --- /dev/null +++ b/cmd/snap/cmd_get_base_declaration_test.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestGetBaseDeclaration(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"get-base-declaration"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": {"base-declaration": "hello"}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"debug", "get-base-declaration"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "hello\n") + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_get_test.go b/cmd/snap/cmd_get_test.go new file mode 100644 index 00000000..b64e3168 --- /dev/null +++ b/cmd/snap/cmd_get_test.go @@ -0,0 +1,226 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "strings" + + . "gopkg.in/check.v1" + + snapset "github.com/snapcore/snapd/cmd/snap" +) + +type getCmdArgs struct { + args, stdout, stderr, error string + isTerminal bool +} + +var getTests = []getCmdArgs{{ + args: "get snap-name --foo", + error: ".*unknown flag.*foo.*", +}, { + args: "get snapname test-key1", + stdout: "test-value1\n", +}, { + args: "get snapname test-key2", + stdout: "2\n", +}, { + args: "get snapname missing-key", + stdout: "\n", +}, { + args: "get -t snapname test-key1", + stdout: "\"test-value1\"\n", +}, { + args: "get -t snapname test-key2", + stdout: "2\n", +}, { + args: "get -t snapname missing-key", + stdout: "null\n", +}, { + args: "get -d snapname test-key1", + stdout: "{\n\t\"test-key1\": \"test-value1\"\n}\n", +}, { + args: "get -l snapname test-key1", + stdout: "Key Value\ntest-key1 test-value1\n", +}, { + args: "get snapname -l test-key1 test-key2", + stdout: "Key Value\ntest-key1 test-value1\ntest-key2 2\n", +}, { + args: "get snapname document", + stderr: `WARNING: The output of "snap get" will become a list with columns - use -d or -l to force the output format.\n`, + stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", +}, { + isTerminal: true, + args: "get snapname document", + stdout: "Key Value\ndocument.key1 value1\ndocument.key2 value2\n", +}, { + args: "get snapname -d test-key1 test-key2", + stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", +}, { + args: "get snapname -l document", + stdout: "Key Value\ndocument.key1 value1\ndocument.key2 value2\n", +}, { + args: "get -d snapname document", + stdout: "{\n\t\"document\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", +}, { + args: "get -l snapname", + stdout: "Key Value\nbar 100\nfoo {...}\n", +}, { + args: "get snapname -l test-key3 test-key4", + stdout: "Key Value\ntest-key3.a 1\ntest-key3.b 2\ntest-key3-a 9\ntest-key4.a 3\ntest-key4.b 4\n", +}, { + args: "get -d snapname", + stdout: "{\n\t\"bar\": 100,\n\t\"foo\": {\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\"\n\t}\n}\n", +}, { + isTerminal: true, + args: "get snapname test-key1 test-key2", + stdout: "Key Value\ntest-key1 test-value1\ntest-key2 2\n", +}, { + isTerminal: false, + args: "get snapname test-key1 test-key2", + stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", + stderr: `WARNING: The output of "snap get" will become a list with columns - use -d or -l to force the output format.\n`, +}, +} + +func (s *SnapSuite) runTests(cmds []getCmdArgs, c *C) { + for _, test := range cmds { + s.stdout.Truncate(0) + s.stderr.Truncate(0) + + c.Logf("Test: %s", test.args) + + restore := snapset.MockIsTerminal(test.isTerminal) + defer restore() + + _, err := snapset.Parser().ParseArgs(strings.Fields(test.args)) + if test.error != "" { + c.Check(err, ErrorMatches, test.error) + } else { + c.Check(err, IsNil) + c.Check(s.Stderr(), Equals, test.stderr) + c.Check(s.Stdout(), Equals, test.stdout) + } + } +} + +func (s *SnapSuite) TestSnapGetTests(c *C) { + s.mockGetConfigServer(c) + s.runTests(getTests, c) +} + +var getNoConfigTests = []getCmdArgs{{ + args: "get -l snapname", + error: `snap "snapname" has no configuration`, +}, { + args: "get snapname", + error: `snap "snapname" has no configuration`, +}, { + args: "get -d snapname", + stdout: "{}\n", +}} + +func (s *SnapSuite) TestSnapGetNoConfiguration(c *C) { + s.mockGetEmptyConfigServer(c) + s.runTests(getNoConfigTests, c) +} + +func (s *SnapSuite) TestSortByPath(c *C) { + values := []snapset.ConfigValue{ + {Path: "test-key3.b"}, + {Path: "a"}, + {Path: "test-key3.a"}, + {Path: "a.b.c"}, + {Path: "test-key4.a"}, + {Path: "test-key4.b"}, + {Path: "a-b"}, + {Path: "zzz"}, + {Path: "aa"}, + {Path: "test-key3-a"}, + {Path: "a.b"}, + } + snapset.SortByPath(values) + + expected := []string{ + "a", + "a.b", + "a.b.c", + "a-b", + "aa", + "test-key3.a", + "test-key3.b", + "test-key3-a", + "test-key4.a", + "test-key4.b", + "zzz", + } + + c.Assert(values, HasLen, len(expected)) + + for i, e := range expected { + c.Assert(values[i].Path, Equals, e) + } +} + +func (s *SnapSuite) mockGetConfigServer(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v2/snaps/snapname/conf" { + c.Errorf("unexpected path %q", r.URL.Path) + return + } + + c.Check(r.Method, Equals, "GET") + + query := r.URL.Query() + switch query.Get("keys") { + case "test-key1": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1"}}`) + case "test-key2": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key2":2}}`) + case "test-key1,test-key2": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key1":"test-value1","test-key2":2}}`) + case "test-key3,test-key4": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"test-key3":{"a":1,"b":2},"test-key3-a":9,"test-key4":{"a":3,"b":4}}}`) + case "missing-key": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`) + case "document": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"document":{"key1":"value1","key2":"value2"}}}`) + case "": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {"foo":{"key1":"value1","key2":"value2"},"bar":100}}`) + default: + c.Errorf("unexpected keys %q", query.Get("keys")) + } + }) +} + +func (s *SnapSuite) mockGetEmptyConfigServer(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v2/snaps/snapname/conf" { + c.Errorf("unexpected path %q", r.URL.Path) + return + } + + c.Check(r.Method, Equals, "GET") + + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`) + }) +} diff --git a/cmd/snap/cmd_help.go b/cmd/snap/cmd_help.go new file mode 100644 index 00000000..97336092 --- /dev/null +++ b/cmd/snap/cmd_help.go @@ -0,0 +1,62 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "os" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortHelpHelp = i18n.G("Help") +var longHelpHelp = i18n.G(` +The help command shows helpful information. Unlike this. ;-) +`) + +type cmdHelp struct { + Manpage bool `long:"man"` + parser *flags.Parser +} + +func init() { + addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} }, + map[string]string{"man": i18n.G("Generate the manpage")}, nil) +} + +func (cmd *cmdHelp) setParser(parser *flags.Parser) { + cmd.parser = parser +} + +func (cmd cmdHelp) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if cmd.Manpage { + cmd.parser.WriteManPage(Stdout) + os.Exit(0) + } + + return &flags.Error{ + Type: flags.ErrHelp, + } +} diff --git a/cmd/snap/cmd_help_test.go b/cmd/snap/cmd_help_test.go new file mode 100644 index 00000000..2d24d7c0 --- /dev/null +++ b/cmd/snap/cmd_help_test.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "os" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestHelpPrintsHelp(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + for _, cmdLine := range [][]string{ + {"snap", "help"}, + {"snap", "--help"}, + {"snap", "-h"}, + } { + os.Args = cmdLine + + err := snap.RunMain() + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?smU)Usage: + +snap \[OPTIONS\] + +Install, configure, refresh and remove snap packages. Snaps are +'universal' packages that work across many different Linux systems, +enabling secure distribution of the latest apps and utilities for +cloud, servers, desktops and the internet of things. + +This is the CLI for snapd, a background service that takes care of +snaps on the system. Start with 'snap list' to see installed snaps. + + +Application Options: + +--version +Print the version and exit + +Help Options: + +-h, --help +Show this help message + +Available commands: + +abort.* +`) + c.Check(s.Stderr(), check.Equals, "") + } +} + +func (s *SnapSuite) TestSubCommandHelpPrintsHelp(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + os.Args = []string{"snap", "install", "--help"} + + err := snap.RunMain() + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?smU)Usage: + +snap \[OPTIONS\] install \[install-OPTIONS\] ... +.* +`) + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_info.go b/cmd/snap/cmd_info.go new file mode 100644 index 00000000..b19de0fd --- /dev/null +++ b/cmd/snap/cmd_info.go @@ -0,0 +1,367 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "bytes" + "fmt" + "io" + "path/filepath" + "strings" + "text/tabwriter" + + "gopkg.in/yaml.v2" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" +) + +type infoCmd struct { + Verbose bool `long:"verbose"` + Positional struct { + Snaps []anySnapName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +var shortInfoHelp = i18n.G("show detailed information about a snap") +var longInfoHelp = i18n.G(` +The info command shows detailed information about a snap, be it by name or by path.`) + +func init() { + addCommand("info", + shortInfoHelp, + longInfoHelp, + func() flags.Commander { + return &infoCmd{} + }, map[string]string{ + "verbose": i18n.G("Include a verbose list of a snap's notes (otherwise, summarise notes)"), + }, nil) +} + +func norm(path string) string { + path = filepath.Clean(path) + if osutil.IsDirectory(path) { + path = path + "/" + } + + return path +} + +func maybePrintPrice(w io.Writer, snap *client.Snap, resInfo *client.ResultInfo) { + if resInfo == nil { + return + } + price, currency, err := getPrice(snap.Prices, resInfo.SuggestedCurrency) + if err != nil { + return + } + fmt.Fprintf(w, "price:\t%s\n", formatPrice(price, currency)) +} + +func maybePrintType(w io.Writer, t string) { + // XXX: using literals here until we reshuffle snap & client properly + // (and os->core rename happens, etc) + switch t { + case "", "app", "application": + return + case "os": + t = "core" + } + + fmt.Fprintf(w, "type:\t%s\n", t) +} + +func maybePrintID(w io.Writer, snap *client.Snap) { + if snap.ID != "" { + fmt.Fprintf(w, "snap-id:\t%s\n", snap.ID) + } +} + +func tryDirect(w io.Writer, path string, verbose bool) bool { + path = norm(path) + + snapf, err := snap.Open(path) + if err != nil { + return false + } + + var sha3_384 string + if verbose && !osutil.IsDirectory(path) { + var err error + sha3_384, _, err = asserts.SnapFileSHA3_384(path) + if err != nil { + return false + } + } + + info, err := snap.ReadInfoFromSnapFile(snapf, nil) + if err != nil { + return false + } + fmt.Fprintf(w, "path:\t%q\n", path) + fmt.Fprintf(w, "name:\t%s\n", info.Name()) + fmt.Fprintf(w, "summary:\t%s\n", formatSummary(info.Summary())) + + var notes *Notes + if verbose { + fmt.Fprintln(w, "notes:\t") + fmt.Fprintf(w, " confinement:\t%s\n", info.Confinement) + if info.Broken == "" { + fmt.Fprintln(w, " broken:\tfalse") + } else { + fmt.Fprintf(w, " broken:\ttrue (%s)\n", info.Broken) + } + + } else { + notes = NotesFromInfo(info) + } + fmt.Fprintf(w, "version:\t%s %s\n", info.Version, notes) + maybePrintType(w, string(info.Type)) + if sha3_384 != "" { + fmt.Fprintf(w, "sha3-384:\t%s\n", sha3_384) + } + + return true +} + +func coalesce(snaps ...*client.Snap) *client.Snap { + for _, s := range snaps { + if s != nil { + return s + } + } + return nil +} + +// formatDescr formats a given string (typically a snap description) +// in a user friendly way. +// +// The rules are (intentionally) very simple: +// - trim whitespace +// - word wrap at "max" chars +// - keep \n intact and break here +// - ignore \r +func formatDescr(descr string, max int) string { + out := bytes.NewBuffer(nil) + for _, line := range strings.Split(strings.TrimSpace(descr), "\n") { + if len(line) > max { + for _, chunk := range strutil.WordWrap(line, max) { + fmt.Fprintf(out, " %s\n", chunk) + } + } else { + fmt.Fprintf(out, " %s\n", line) + } + } + + return strings.TrimSuffix(out.String(), "\n") +} + +func maybePrintCommands(w io.Writer, snapName string, allApps []client.AppInfo, n int) { + if len(allApps) == 0 { + return + } + + commands := make([]string, 0, len(allApps)) + for _, app := range allApps { + if app.IsService() { + continue + } + + cmdStr := snap.JoinSnapApp(snapName, app.Name) + commands = append(commands, cmdStr) + } + if len(commands) == 0 { + return + } + + fmt.Fprintf(w, "commands:\n") + for _, cmd := range commands { + fmt.Fprintf(w, " - %s\n", cmd) + } +} + +func maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, n int) { + if len(allApps) == 0 { + return + } + + services := make([]string, 0, len(allApps)) + for _, app := range allApps { + if !app.IsService() { + continue + } + + var active, enabled string + if app.Active { + active = "active" + } else { + active = "inactive" + } + if app.Enabled { + enabled = "enabled" + } else { + enabled = "disabled" + } + services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(snapName, app.Name), app.Daemon, enabled, active)) + } + if len(services) == 0 { + return + } + + fmt.Fprintf(w, "services:\n") + for _, svc := range services { + fmt.Fprintln(w, svc) + } +} + +// displayChannels displays channels and tracks in the right order +func displayChannels(w io.Writer, remote *client.Snap) { + // \t\t\t so we get "installed" lined up with "channels" + fmt.Fprintf(w, "channels:\t\t\t\n") + + // order by tracks + for _, tr := range remote.Tracks { + trackHasOpenChannel := false + for _, risk := range []string{"stable", "candidate", "beta", "edge"} { + chName := fmt.Sprintf("%s/%s", tr, risk) + ch, ok := remote.Channels[chName] + if tr == "latest" { + chName = risk + } + var version, revision, size, notes string + if ok { + version = ch.Version + revision = fmt.Sprintf("(%s)", ch.Revision) + size = strutil.SizeToStr(ch.Size) + notes = NotesFromChannelSnapInfo(ch).String() + trackHasOpenChannel = true + } else { + if trackHasOpenChannel { + version = "↑" + } else { + version = "–" // that's an en dash (so yaml is happy) + } + } + fmt.Fprintf(w, " %s:\t%s\t%s\t%s\t%s\n", chName, version, revision, size, notes) + } + } +} + +func formatSummary(raw string) string { + s, err := yaml.Marshal(raw) + if err != nil { + return fmt.Sprintf("cannot marshal summary: %s", err) + } + return strings.TrimSpace(string(s)) +} + +func (x *infoCmd) Execute([]string) error { + cli := Client() + + w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) + + noneOK := true + for i, snapName := range x.Positional.Snaps { + snapName := string(snapName) + if i > 0 { + fmt.Fprintln(w, "---") + } + + if tryDirect(w, snapName, x.Verbose) { + noneOK = false + continue + } + remote, resInfo, _ := cli.FindOne(snapName) + local, _, _ := cli.Snap(snapName) + + both := coalesce(local, remote) + + if both == nil { + fmt.Fprintf(w, "argument:\t%q\nwarning:\t%s\n", snapName, i18n.G("not a valid snap")) + continue + } + noneOK = false + + fmt.Fprintf(w, "name:\t%s\n", both.Name) + fmt.Fprintf(w, "summary:\t%s\n", formatSummary(both.Summary)) + // TODO: have publisher; use publisher here, + // and additionally print developer if publisher != developer + fmt.Fprintf(w, "publisher:\t%s\n", both.Developer) + if both.Contact != "" { + fmt.Fprintf(w, "contact:\t%s\n", strings.TrimPrefix(both.Contact, "mailto:")) + } + maybePrintPrice(w, remote, resInfo) + // FIXME: find out for real + termWidth := 77 + fmt.Fprintf(w, "description: |\n%s\n", formatDescr(both.Description, termWidth)) + maybePrintType(w, both.Type) + maybePrintID(w, both) + maybePrintCommands(w, snapName, both.Apps, termWidth) + maybePrintServices(w, snapName, both.Apps, termWidth) + + if x.Verbose { + fmt.Fprintln(w, "notes:\t") + fmt.Fprintf(w, " private:\t%t\n", both.Private) + fmt.Fprintf(w, " confinement:\t%s\n", both.Confinement) + } + + if local != nil { + var notes *Notes + if x.Verbose { + jailMode := local.Confinement == client.DevModeConfinement && !local.DevMode + fmt.Fprintf(w, " devmode:\t%t\n", local.DevMode) + fmt.Fprintf(w, " jailmode:\t%t\n", jailMode) + fmt.Fprintf(w, " trymode:\t%t\n", local.TryMode) + fmt.Fprintf(w, " enabled:\t%t\n", local.Status == client.StatusActive) + if local.Broken == "" { + fmt.Fprintf(w, " broken:\t%t\n", false) + } else { + fmt.Fprintf(w, " broken:\t%t (%s)\n", true, local.Broken) + } + + fmt.Fprintf(w, " ignore-validation:\t%t\n", local.IgnoreValidation) + } else { + notes = NotesFromLocal(local) + } + + fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) + fmt.Fprintf(w, "installed:\t%s\t(%s)\t%s\t%s\n", local.Version, local.Revision, strutil.SizeToStr(local.InstalledSize), notes) + fmt.Fprintf(w, "refreshed:\t%s\n", local.InstallDate) + } + + if remote != nil && remote.Channels != nil && remote.Tracks != nil { + displayChannels(w, remote) + } + + } + w.Flush() + + if noneOK { + return fmt.Errorf(i18n.G("no valid snaps given")) + } + + return nil +} diff --git a/cmd/snap/cmd_info_test.go b/cmd/snap/cmd_info_test.go new file mode 100644 index 00000000..c068d36d --- /dev/null +++ b/cmd/snap/cmd_info_test.go @@ -0,0 +1,187 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "bytes" + "fmt" + "net/http" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +var cmdAppInfos = []client.AppInfo{{Name: "app1"}, {Name: "app2"}} +var svcAppInfos = []client.AppInfo{ + { + Name: "svc1", + Daemon: "simple", + Enabled: false, + Active: true, + }, + { + Name: "svc2", + Daemon: "simple", + Enabled: true, + Active: false, + }, +} + +var mixedAppInfos = append(append([]client.AppInfo(nil), cmdAppInfos...), svcAppInfos...) + +func (s *SnapSuite) TestMaybePrintServices(c *check.C) { + for _, infos := range [][]client.AppInfo{svcAppInfos, mixedAppInfos} { + var buf bytes.Buffer + snap.MaybePrintServices(&buf, "foo", infos, -1) + + c.Check(buf.String(), check.Equals, `services: + foo.svc1: simple, disabled, active + foo.svc2: simple, enabled, inactive +`) + } +} + +func (s *SnapSuite) TestMaybePrintServicesNoServices(c *check.C) { + for _, infos := range [][]client.AppInfo{cmdAppInfos, nil} { + var buf bytes.Buffer + snap.MaybePrintServices(&buf, "foo", infos, -1) + + c.Check(buf.String(), check.Equals, "") + } +} + +func (s *SnapSuite) TestMaybePrintCommands(c *check.C) { + for _, infos := range [][]client.AppInfo{cmdAppInfos, mixedAppInfos} { + var buf bytes.Buffer + snap.MaybePrintCommands(&buf, "foo", infos, -1) + + c.Check(buf.String(), check.Equals, `commands: + - foo.app1 + - foo.app2 +`) + } +} + +func (s *SnapSuite) TestMaybePrintCommandsNoCommands(c *check.C) { + for _, infos := range [][]client.AppInfo{svcAppInfos, nil} { + var buf bytes.Buffer + snap.MaybePrintCommands(&buf, "foo", infos, -1) + + c.Check(buf.String(), check.Equals, "") + } +} + +func (s *SnapSuite) TestInfoPriced(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, findPricedJSON) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, "{}") + default: + c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: GNU Hello, the "hello world" snap +publisher: canonical +price: 1.99GBP +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +`) + c.Check(s.Stderr(), check.Equals, "") +} + +const mockInfoJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "The GNU Hello snap", + "type": "app", + "version": "2.10" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestInfoUnquoted(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + fmt.Fprintln(w, mockInfoJSON) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, "{}") + default: + c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: canonical +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +`) + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_interface.go b/cmd/snap/cmd_interface.go new file mode 100644 index 00000000..a2cbb38b --- /dev/null +++ b/cmd/snap/cmd_interface.go @@ -0,0 +1,186 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "sort" + "text/tabwriter" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdInterface struct { + ShowAttrs bool `long:"attrs"` + ShowAll bool `long:"all"` + Positionals struct { + Interface interfaceName `skip-help:"true"` + } `positional-args:"true"` +} + +var shortInterfaceHelp = i18n.G("Lists snap interfaces") +var longInterfaceHelp = i18n.G(` +The interface command shows details of snap interfaces. + +If no interface name is provided, a list of interface names with at least +one connection is shown, or a list of all interfaces if --all is provided. +`) + +func init() { + addCommand("interface", shortInterfaceHelp, longInterfaceHelp, func() flags.Commander { + return &cmdInterface{} + }, map[string]string{ + "attrs": i18n.G("Show interface attributes"), + "all": i18n.G("Include unused interfaces"), + }, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Show details of a specific interface"), + }}) +} + +func (x *cmdInterface) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if x.Positionals.Interface != "" { + // Show one interface in detail. + name := string(x.Positionals.Interface) + ifaces, err := Client().Interfaces(&client.InterfaceOptions{ + Names: []string{name}, + Doc: true, + Plugs: true, + Slots: true, + }) + if err != nil { + return err + } + if len(ifaces) == 0 { + return fmt.Errorf(i18n.G("no such interface")) + } + x.showOneInterface(ifaces[0]) + } else { + // Show an overview of available interfaces. + ifaces, err := Client().Interfaces(&client.InterfaceOptions{ + Connected: !x.ShowAll, + }) + if err != nil { + return err + } + if len(ifaces) == 0 { + if x.ShowAll { + return fmt.Errorf(i18n.G("no interfaces found")) + } + return fmt.Errorf(i18n.G("no interfaces currently connected")) + } + x.showManyInterfaces(ifaces) + } + return nil +} + +func (x *cmdInterface) showOneInterface(iface *client.Interface) { + w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) + defer w.Flush() + + fmt.Fprintf(w, "name:\t%s\n", iface.Name) + if iface.Summary != "" { + fmt.Fprintf(w, "summary:\t%s\n", iface.Summary) + } + if iface.DocURL != "" { + fmt.Fprintf(w, "documentation:\t%s\n", iface.DocURL) + } + if len(iface.Plugs) > 0 { + fmt.Fprintf(w, "plugs:\n") + for _, plug := range iface.Plugs { + var labelPart string + if plug.Label != "" { + labelPart = fmt.Sprintf(" (%s)", plug.Label) + } + if plug.Name == iface.Name { + fmt.Fprintf(w, " - %s%s", plug.Snap, labelPart) + } else { + fmt.Fprintf(w, ` - %s:%s%s`, plug.Snap, plug.Name, labelPart) + } + // Print a colon which will make the snap:plug element a key-value + // yaml object so that we can write the attributes. + if len(plug.Attrs) > 0 && x.ShowAttrs { + fmt.Fprintf(w, ":\n") + x.showAttrs(w, plug.Attrs, " ") + } else { + fmt.Fprintf(w, "\n") + } + } + } + if len(iface.Slots) > 0 { + fmt.Fprintf(w, "slots:\n") + for _, slot := range iface.Slots { + var labelPart string + if slot.Label != "" { + labelPart = fmt.Sprintf(" (%s)", slot.Label) + } + if slot.Name == iface.Name { + fmt.Fprintf(w, " - %s%s", slot.Snap, labelPart) + } else { + fmt.Fprintf(w, ` - %s:%s%s`, slot.Snap, slot.Name, labelPart) + } + // Print a colon which will make the snap:slot element a key-value + // yaml object so that we can write the attributes. + if len(slot.Attrs) > 0 && x.ShowAttrs { + fmt.Fprintf(w, ":\n") + x.showAttrs(w, slot.Attrs, " ") + } else { + fmt.Fprintf(w, "\n") + } + } + } +} + +func (x *cmdInterface) showManyInterfaces(infos []*client.Interface) { + w := tabWriter() + defer w.Flush() + fmt.Fprintln(w, i18n.G("Name\tSummary")) + for _, iface := range infos { + fmt.Fprintf(w, "%s\t%s\n", iface.Name, iface.Summary) + } +} + +func (x *cmdInterface) showAttrs(w io.Writer, attrs map[string]interface{}, indent string) { + if len(attrs) == 0 { + return + } + names := make([]string, 0, len(attrs)) + for name := range attrs { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + value := attrs[name] + switch value.(type) { + case string, int, bool: + fmt.Fprintf(w, "%s %s:\t%v\n", indent, name, value) + } + } +} diff --git a/cmd/snap/cmd_interface_test.go b/cmd/snap/cmd_interface_test.go new file mode 100644 index 00000000..539547b8 --- /dev/null +++ b/cmd/snap/cmd_interface_test.go @@ -0,0 +1,293 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "net/http" + "os" + + "github.com/jessevdk/go-flags" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestInterfaceHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] interface [interface-OPTIONS] [] + +The interface command shows details of snap interfaces. + +If no interface name is provided, a list of interface names with at least +one connection is shown, or a list of all interfaces if --all is provided. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message + +[interface command options] + --attrs Show interface attributes + --all Include unused interfaces + +[interface command arguments] + : Show details of a specific interface +` + rest, err := Parser().ParseArgs([]string{"interface", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestInterfaceListEmpty(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.RawQuery, Equals, "select=connected") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []*client.Interface{}, + }) + }) + rest, err := Parser().ParseArgs([]string{"interface"}) + c.Assert(err, ErrorMatches, "no interfaces currently connected") + c.Assert(rest, DeepEquals, []string{"interface"}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceListAllEmpty(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.RawQuery, Equals, "select=all") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []*client.Interface{}, + }) + }) + rest, err := Parser().ParseArgs([]string{"interface", "--all"}) + c.Assert(err, ErrorMatches, "no interfaces found") + c.Assert(rest, DeepEquals, []string{"--all"}) // XXX: feels like a bug in go-flags. + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceList(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.RawQuery, Equals, "select=connected") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []*client.Interface{{ + Name: "network", + Summary: "allows access to the network", + }, { + Name: "network-bind", + Summary: "allows providing services on the network", + }}, + }) + }) + rest, err := Parser().ParseArgs([]string{"interface"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Name Summary\n" + + "network allows access to the network\n" + + "network-bind allows providing services on the network\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceListAll(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.RawQuery, Equals, "select=all") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []*client.Interface{{ + Name: "network", + Summary: "allows access to the network", + }, { + Name: "network-bind", + Summary: "allows providing services on the network", + }, { + Name: "unused", + Summary: "just an unused interface, nothing to see here", + }}, + }) + }) + rest, err := Parser().ParseArgs([]string{"interface", "--all"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Name Summary\n" + + "network allows access to the network\n" + + "network-bind allows providing services on the network\n" + + "unused just an unused interface, nothing to see here\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceDetails(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.RawQuery, Equals, "doc=true&names=network&plugs=true&select=all&slots=true") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []*client.Interface{{ + Name: "network", + Summary: "allows access to the network", + DocURL: "http://example.org/about-the-network-interface", + Plugs: []client.Plug{ + {Snap: "deepin-music", Name: "network"}, + {Snap: "http", Name: "network"}, + }, + Slots: []client.Slot{{Snap: "core", Name: "network"}}, + }}, + }) + }) + rest, err := Parser().ParseArgs([]string{"interface", "network"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "name: network\n" + + "summary: allows access to the network\n" + + "documentation: http://example.org/about-the-network-interface\n" + + "plugs:\n" + + " - deepin-music\n" + + " - http\n" + + "slots:\n" + + " - core\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceDetailsAndAttrs(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.RawQuery, Equals, "doc=true&names=serial-port&plugs=true&select=all&slots=true") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []*client.Interface{{ + Name: "serial-port", + Summary: "allows providing or using a specific serial port", + Plugs: []client.Plug{ + {Snap: "minicom", Name: "serial-port"}, + }, + Slots: []client.Slot{{ + Snap: "gizmo-gadget", + Name: "debug-serial-port", + Label: "serial port for debugging", + Attrs: map[string]interface{}{ + "header": "pin-array", + "location": "internal", + "path": "/dev/ttyS0", + }, + }}, + }}, + }) + }) + rest, err := Parser().ParseArgs([]string{"interface", "--attrs", "serial-port"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "name: serial-port\n" + + "summary: allows providing or using a specific serial port\n" + + "plugs:\n" + + " - minicom\n" + + "slots:\n" + + " - gizmo-gadget:debug-serial-port (serial port for debugging):\n" + + " header: pin-array\n" + + " location: internal\n" + + " path: /dev/ttyS0\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestInterfaceCompletion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.RawQuery, Equals, "select=all") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": []*client.Interface{{ + Name: "network", + Summary: "allows access to the network", + }, { + Name: "network-bind", + Summary: "allows providing services on the network", + }}, + }) + }) + os.Setenv("GO_FLAGS_COMPLETION", "verbose") + defer os.Unsetenv("GO_FLAGS_COMPLETION") + + expected := []flags.Completion{} + parser := Parser() + parser.CompletionHandler = func(obtained []flags.Completion) { + c.Check(obtained, DeepEquals, expected) + } + + expected = []flags.Completion{ + {Item: "network", Description: "allows access to the network"}, + {Item: "network-bind", Description: "allows providing services on the network"}, + } + _, err := parser.ParseArgs([]string{"interface", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{ + {Item: "network-bind", Description: "allows providing services on the network"}, + } + _, err = parser.ParseArgs([]string{"interface", "network-"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{} + _, err = parser.ParseArgs([]string{"interface", "bogus"}) + c.Assert(err, IsNil) + + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_interfaces.go b/cmd/snap/cmd_interfaces.go new file mode 100644 index 00000000..ebe80d6d --- /dev/null +++ b/cmd/snap/cmd_interfaces.go @@ -0,0 +1,141 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdInterfaces struct { + Interface string `short:"i"` + Positionals struct { + Query interfacesSlotOrPlugSpec `skip-help:"true"` + } `positional-args:"true"` +} + +var shortInterfacesHelp = i18n.G("Lists interfaces in the system") +var longInterfacesHelp = i18n.G(` +The interfaces command lists interfaces available in the system. + +By default all slots and plugs, used and offered by all snaps, are displayed. + +$ snap interfaces : + +Lists only the specified slot or plug. + +$ snap interfaces + +Lists the slots offered and plugs used by the specified snap. + +$ snap interfaces -i= [] + +Filters the complete output so only plugs and/or slots matching the provided details are listed. +`) + +func init() { + addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander { + return &cmdInterfaces{} + }, map[string]string{ + "i": i18n.G("Constrain listing to specific interfaces"), + }, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(":"), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Constrain listing to a specific snap or snap:name"), + }}) +} + +func (x *cmdInterfaces) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + ifaces, err := Client().Connections() + if err != nil { + return err + } + if len(ifaces.Plugs) == 0 && len(ifaces.Slots) == 0 { + return fmt.Errorf(i18n.G("no interfaces found")) + } + w := tabWriter() + defer w.Flush() + fmt.Fprintln(w, i18n.G("Slot\tPlug")) + for _, slot := range ifaces.Slots { + if wanted := x.Positionals.Query.Snap; wanted != "" { + ok := wanted == slot.Snap + for i := 0; i < len(slot.Connections) && !ok; i++ { + ok = wanted == slot.Connections[i].Snap + } + if !ok { + continue + } + } + if x.Positionals.Query.Name != "" && x.Positionals.Query.Name != slot.Name { + continue + } + if x.Interface != "" && slot.Interface != x.Interface { + continue + } + // The OS snap is special and enable abbreviated + // display syntax on the slot-side of the connection. + if slot.Snap == "core" || slot.Snap == "ubuntu-core" { + fmt.Fprintf(w, ":%s\t", slot.Name) + } else { + fmt.Fprintf(w, "%s:%s\t", slot.Snap, slot.Name) + } + for i := 0; i < len(slot.Connections); i++ { + if i > 0 { + fmt.Fprint(w, ",") + } + if slot.Connections[i].Name != slot.Name { + fmt.Fprintf(w, "%s:%s", slot.Connections[i].Snap, slot.Connections[i].Name) + } else { + fmt.Fprintf(w, "%s", slot.Connections[i].Snap) + } + } + // Display visual indicator for disconnected slots + if len(slot.Connections) == 0 { + fmt.Fprint(w, "-") + } + fmt.Fprintf(w, "\n") + } + // Plugs are treated differently. Since the loop above already printed each connected + // plug, the loop below focuses on printing just the disconnected plugs. + for _, plug := range ifaces.Plugs { + if x.Positionals.Query.Snap != "" && x.Positionals.Query.Snap != plug.Snap { + continue + } + if x.Positionals.Query.Name != "" && x.Positionals.Query.Name != plug.Name { + continue + } + if x.Interface != "" && plug.Interface != x.Interface { + continue + } + // Display visual indicator for disconnected plugs. + if len(plug.Connections) == 0 { + fmt.Fprintf(w, "-\t%s:%s\n", plug.Snap, plug.Name) + } + } + return nil +} diff --git a/cmd/snap/cmd_interfaces_test.go b/cmd/snap/cmd_interfaces_test.go new file mode 100644 index 00000000..eaa5eaf5 --- /dev/null +++ b/cmd/snap/cmd_interfaces_test.go @@ -0,0 +1,574 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "io/ioutil" + "net/http" + "os" + + "github.com/jessevdk/go-flags" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConnectionsZeroSlotsOnePlug(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Plugs: []client.Plug{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "- keyboard-lights:capslock-led\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsZeroPlugsOneSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:pin-13 -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsOneSlotOnePlug(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.PlugRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }, + Plugs: []client.Plug{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "bool-file", + Label: "Capslock indicator LED", + Connections: []client.SlotRef{ + { + Snap: "canonical-pi2", + Name: "pin-13", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:pin-13 keyboard-lights:capslock-led\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") + + s.SetUpTest(c) + // should be the same + rest, err = Parser().ParseArgs([]string{"interfaces", "canonical-pi2"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") + + s.SetUpTest(c) + // and the same again + rest, err = Parser().ParseArgs([]string{"interfaces", "keyboard-lights"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsTwoPlugs(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.PlugRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + { + Snap: "keyboard-lights", + Name: "scrollock-led", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:pin-13 keyboard-lights:capslock-led,keyboard-lights:scrollock-led\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsPlugsWithCommonName(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.PlugRef{ + { + Snap: "paste-daemon", + Name: "network-listening", + }, + { + Snap: "time-daemon", + Name: "network-listening", + }, + }, + }, + }, + Plugs: []client.Plug{ + { + Snap: "paste-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "canonical-pi2", + Name: "network-listening", + }, + }, + }, + { + Snap: "time-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "canonical-pi2", + Name: "network-listening", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:network-listening paste-daemon,time-daemon\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsOsSnapSlots(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "core", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.PlugRef{ + { + Snap: "paste-daemon", + Name: "network-listening", + }, + { + Snap: "time-daemon", + Name: "network-listening", + }, + }, + }, + }, + Plugs: []client.Plug{ + { + Snap: "paste-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "core", + Name: "network-listening", + }, + }, + }, + { + Snap: "time-daemon", + Name: "network-listening", + Interface: "network-listening", + Label: "Ability to be a network service", + Connections: []client.SlotRef{ + { + Snap: "core", + Name: "network-listening", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + ":network-listening paste-daemon,time-daemon\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsTwoSlotsAndFiltering(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "canonical-pi2", + Name: "debug-console", + Interface: "serial-port", + Label: "Serial port on the expansion header", + Connections: []client.PlugRef{ + { + Snap: "core", + Name: "debug-console", + }, + }, + }, + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.PlugRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces", "-i=serial-port"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "canonical-pi2:debug-console core\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsOfSpecificSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "cheese", + Name: "photo-trigger", + Interface: "bool-file", + Label: "Photo trigger", + }, + { + Snap: "wake-up-alarm", + Name: "toggle", + Interface: "bool-file", + Label: "Alarm toggle", + }, + { + Snap: "wake-up-alarm", + Name: "snooze", + Interface: "bool-file", + Label: "Alarm snooze", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces", "wake-up-alarm"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "wake-up-alarm:toggle -\n" + + "wake-up-alarm:snooze -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsOfSpecificSnapAndSlot(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "cheese", + Name: "photo-trigger", + Interface: "bool-file", + Label: "Photo trigger", + }, + { + Snap: "wake-up-alarm", + Name: "toggle", + Interface: "bool-file", + Label: "Alarm toggle", + }, + { + Snap: "wake-up-alarm", + Name: "snooze", + Interface: "bool-file", + Label: "Alarm snooze", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces", "wake-up-alarm:snooze"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "wake-up-alarm:snooze -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsNothingAtAll(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{}, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces"}) + c.Assert(err, ErrorMatches, "no interfaces found") + // XXX: not sure why this is returned, I guess that's what happens when a + // command Execute returns an error. + c.Assert(rest, DeepEquals, []string{"interfaces"}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsOfSpecificType(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/interfaces") + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": client.Connections{ + Slots: []client.Slot{ + { + Snap: "cheese", + Name: "photo-trigger", + Interface: "bool-file", + Label: "Photo trigger", + }, + { + Snap: "wake-up-alarm", + Name: "toggle", + Interface: "bool-file", + Label: "Alarm toggle", + }, + { + Snap: "wake-up-alarm", + Name: "snooze", + Interface: "bool-file", + Label: "Alarm snooze", + }, + }, + }, + }) + }) + rest, err := Parser().ParseArgs([]string{"interfaces", "-i", "bool-file"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Slot Plug\n" + + "cheese:photo-trigger -\n" + + "wake-up-alarm:toggle -\n" + + "wake-up-alarm:snooze -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsCompletion(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Assert(r.Method, Equals, "GET") + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": fortestingConnectionList, + }) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + os.Setenv("GO_FLAGS_COMPLETION", "verbose") + defer os.Unsetenv("GO_FLAGS_COMPLETION") + + expected := []flags.Completion{} + parser := Parser() + parser.CompletionHandler = func(obtained []flags.Completion) { + c.Check(obtained, DeepEquals, expected) + } + + expected = []flags.Completion{{Item: "canonical-pi2:"}, {Item: "core:"}, {Item: "keyboard-lights:"}, {Item: "paste-daemon:"}, {Item: "potato:"}, {Item: "wake-up-alarm:"}} + _, err := parser.ParseArgs([]string{"interfaces", ""}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "paste-daemon:network-listening", Description: "plug"}} + _, err = parser.ParseArgs([]string{"interfaces", "pa"}) + c.Assert(err, IsNil) + + expected = []flags.Completion{{Item: "wake-up-alarm:toggle", Description: "slot"}} + _, err = parser.ParseArgs([]string{"interfaces", "wa"}) + c.Assert(err, IsNil) + + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_keys.go b/cmd/snap/cmd_keys.go new file mode 100644 index 00000000..e4206cc2 --- /dev/null +++ b/cmd/snap/cmd_keys.go @@ -0,0 +1,90 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdKeys struct { + JSON bool `long:"json"` +} + +func init() { + cmd := addCommand("keys", + i18n.G("List cryptographic keys"), + i18n.G("List cryptographic keys that can be used for signing assertions."), + func() flags.Commander { + return &cmdKeys{} + }, map[string]string{"json": i18n.G("Output results in JSON format")}, nil) + cmd.hidden = true +} + +// Key represents a key that can be used for signing assertions. +type Key struct { + Name string `json:"name"` + Sha3_384 string `json:"sha3-384"` +} + +func (x *cmdKeys) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + w := tabWriter() + if !x.JSON { + fmt.Fprintln(w, i18n.G("Name\tSHA3-384")) + defer w.Flush() + } + keys := []Key{} + + manager := asserts.NewGPGKeypairManager() + display := func(privk asserts.PrivateKey, fpr string, uid string) error { + key := Key{ + Name: uid, + Sha3_384: privk.PublicKey().ID(), + } + if x.JSON { + keys = append(keys, key) + } else { + fmt.Fprintf(w, "%s\t%s\n", key.Name, key.Sha3_384) + } + return nil + } + err := manager.Walk(display) + if err != nil { + return err + } + if x.JSON { + obj, err := json.Marshal(keys) + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", obj) + } + + return nil +} diff --git a/cmd/snap/cmd_keys_test.go b/cmd/snap/cmd_keys_test.go new file mode 100644 index 00000000..eec332e5 --- /dev/null +++ b/cmd/snap/cmd_keys_test.go @@ -0,0 +1,127 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +type SnapKeysSuite struct { + BaseSnapSuite + + GnupgCmd string +} + +// FIXME: Ideally we would just use gpg2 and remove the gnupg2_test.go file. +// However currently there is LP: #1621839 which prevents us from +// switching to gpg2 fully. Once this is resolved we should switch. +var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg"}) + +var fakePinentryData = []byte(`#!/bin/sh +set -e +echo "OK Pleased to meet you" +while true; do + read line + case $line in + BYE) + exit 0 + ;; + *) + echo "OK I agree to everything" + ;; +esac +done +`) + +func (s *SnapKeysSuite) SetUpTest(c *C) { + s.BaseSnapSuite.SetUpTest(c) + + tempdir := c.MkDir() + for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { + data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644) + c.Assert(err, IsNil) + } + fakePinentryFn := filepath.Join(tempdir, "pinentry-fake") + err := ioutil.WriteFile(fakePinentryFn, fakePinentryData, 0755) + c.Assert(err, IsNil) + gpgAgentConfFn := filepath.Join(tempdir, "gpg-agent.conf") + err = ioutil.WriteFile(gpgAgentConfFn, []byte(fmt.Sprintf(`pinentry-program %s`, fakePinentryFn)), 0644) + c.Assert(err, IsNil) + + os.Setenv("SNAP_GNUPG_HOME", tempdir) + os.Setenv("SNAP_GNUPG_CMD", s.GnupgCmd) +} + +func (s *SnapKeysSuite) TearDownTest(c *C) { + os.Unsetenv("SNAP_GNUPG_HOME") + os.Unsetenv("SNAP_GNUPG_CMD") + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *SnapKeysSuite) TestKeys(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"keys"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, `Name +SHA3-384 +default +g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ +another +DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L +`) + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestKeysJSON(c *C) { + rest, err := snap.Parser().ParseArgs([]string{"keys", "--json"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedResponse := []snap.Key{ + { + Name: "default", + Sha3_384: "g4Pks54W_US4pZuxhgG_RHNAf_UeZBBuZyGRLLmMj1Do3GkE_r_5A5BFjx24ZwVJ", + }, + { + Name: "another", + Sha3_384: "DVQf1U4mIsuzlQqAebjjTPYtYJ-GEhJy0REuj3zvpQYTZ7EJj7adBxIXLJ7Vmk3L", + }, + } + var obtainedResponse []snap.Key + json.Unmarshal(s.stdout.Bytes(), &obtainedResponse) + c.Check(obtainedResponse, DeepEquals, expectedResponse) + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapKeysSuite) TestKeysJSONEmpty(c *C) { + err := os.RemoveAll(os.Getenv("SNAP_GNUPG_HOME")) + c.Assert(err, IsNil) + rest, err := snap.Parser().ParseArgs([]string{"keys", "--json"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "[]\n") + c.Check(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_known.go b/cmd/snap/cmd_known.go new file mode 100644 index 00000000..56f303b1 --- /dev/null +++ b/cmd/snap/cmd_known.go @@ -0,0 +1,129 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/store" + + "github.com/jessevdk/go-flags" +) + +type cmdKnown struct { + KnownOptions struct { + // XXX: how to get a list of assert types for completion? + AssertTypeName assertTypeName `required:"true"` + HeaderFilters []string `required:"0"` + } `positional-args:"true" required:"true"` + + Remote bool `long:"remote"` +} + +var shortKnownHelp = i18n.G("Shows known assertions of the provided type") +var longKnownHelp = i18n.G(` +The known command shows known assertions of the provided type. +If header=value pairs are provided after the assertion type, the assertions +shown must also have the specified headers matching the provided values. +`) + +func init() { + addCommand("known", shortKnownHelp, longKnownHelp, func() flags.Commander { + return &cmdKnown{} + }, nil, []argDesc{ + { + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Assertion type name"), + }, { + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G("
"), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Constrain listing to those matching header=value"), + }, + }) +} + +var nl = []byte{'\n'} + +var storeNew = store.New + +func downloadAssertion(typeName string, headers map[string]string) ([]asserts.Assertion, error) { + var user *auth.UserState + + // FIXME: set auth context + var authContext auth.AuthContext + + at := asserts.Type(typeName) + if at == nil { + return nil, fmt.Errorf("cannot find assertion type %q", typeName) + } + primaryKeys, err := asserts.PrimaryKeyFromHeaders(at, headers) + if err != nil { + return nil, fmt.Errorf("cannot query remote assertion: %v", err) + } + + sto := storeNew(nil, authContext) + as, err := sto.Assertion(at, primaryKeys, user) + if err != nil { + return nil, err + } + + return []asserts.Assertion{as}, nil +} + +func (x *cmdKnown) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + // TODO: share this kind of parsing once it's clearer how often is used in snap + headers := map[string]string{} + for _, headerFilter := range x.KnownOptions.HeaderFilters { + parts := strings.SplitN(headerFilter, "=", 2) + if len(parts) != 2 { + return fmt.Errorf(i18n.G("invalid header filter: %q (want key=value)"), headerFilter) + } + headers[parts[0]] = parts[1] + } + + var assertions []asserts.Assertion + var err error + if x.Remote { + assertions, err = downloadAssertion(string(x.KnownOptions.AssertTypeName), headers) + } else { + assertions, err = Client().Known(string(x.KnownOptions.AssertTypeName), headers) + } + if err != nil { + return err + } + + enc := asserts.NewEncoder(Stdout) + for _, a := range assertions { + enc.Encode(a) + } + + return nil +} diff --git a/cmd/snap/cmd_known_test.go b/cmd/snap/cmd_known_test.go new file mode 100644 index 00000000..ab61dbc8 --- /dev/null +++ b/cmd/snap/cmd_known_test.go @@ -0,0 +1,109 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + + "github.com/jessevdk/go-flags" + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/store" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +// acquire example data via: +// curl -H "accept: application/x.ubuntu.assertion" https://assertions.ubuntu.com/v1/assertions/model/16/canonical/pi2 +const mockModelAssertion = `type: model +authority-id: canonical +series: 16 +brand-id: canonical +model: pi99 +architecture: armhf +gadget: pi99 +kernel: pi99-kernel +timestamp: 2016-08-31T00:00:00.0Z +sign-key-sha3-384: 9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn + +AcLorsomethingthatlooksvaguelylikeasignature== +` + +func (s *SnapSuite) TestKnownRemote(c *check.C) { + var server *httptest.Server + + restorer := snap.MockStoreNew(func(cfg *store.Config, auth auth.AuthContext) *store.Store { + if cfg == nil { + cfg = store.DefaultConfig() + } + serverURL, _ := url.Parse(server.URL) + cfg.AssertionsBaseURL = serverURL + return store.New(cfg, auth) + }) + defer restorer() + + n := 0 + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.URL.Path, check.Matches, ".*/assertions/.*") // sanity check request + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/api/v1/snaps/assertions/model/16/canonical/pi99") + fmt.Fprintln(w, mockModelAssertion) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + })) + + rest, err := snap.Parser().ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, mockModelAssertion) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestKnownRemoteMissingPrimaryKey(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical"}) + c.Assert(err, check.ErrorMatches, `cannot query remote assertion: must provide primary key: model`) +} + +func (s *SnapSuite) TestAssertTypeNameCompletion(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/assertions") + fmt.Fprintln(w, `{"type": "sync", "result": { "types": [ "account", "... more stuff ...", "validation" ] } }`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + + c.Check(snap.AssertTypeNameCompletion("v"), check.DeepEquals, []flags.Completion{{Item: "validation"}}) +} diff --git a/cmd/snap/cmd_list.go b/cmd/snap/cmd_list.go new file mode 100644 index 00000000..d77bf280 --- /dev/null +++ b/cmd/snap/cmd_list.go @@ -0,0 +1,105 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + "sort" + "text/tabwriter" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortListHelp = i18n.G("List installed snaps") +var longListHelp = i18n.G(` +The list command displays a summary of snaps installed in the current system.`) + +type cmdList struct { + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` + + All bool `long:"all"` +} + +func init() { + addCommand("list", shortListHelp, longListHelp, func() flags.Commander { return &cmdList{} }, + map[string]string{"all": i18n.G("Show all revisions")}, nil) +} + +type snapsByName []*client.Snap + +func (s snapsByName) Len() int { return len(s) } +func (s snapsByName) Less(i, j int) bool { return s[i].Name < s[j].Name } +func (s snapsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func (x *cmdList) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + names := make([]string, len(x.Positional.Snaps)) + for i, name := range x.Positional.Snaps { + names[i] = string(name) + } + + return listSnaps(names, x.All) +} + +var ErrNoMatchingSnaps = errors.New(i18n.G("no matching snaps installed")) + +func listSnaps(names []string, all bool) error { + cli := Client() + snaps, err := cli.List(names, &client.ListOptions{All: all}) + if err != nil { + if err == client.ErrNoSnapsInstalled { + if len(names) == 0 { + fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try \"snap install hello-world\".")) + return nil + } else { + return ErrNoMatchingSnaps + } + } + return err + } else if len(snaps) == 0 { + return ErrNoMatchingSnaps + } + sort.Sort(snapsByName(snaps)) + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Name\tVersion\tRev\tDeveloper\tNotes")) + + for _, snap := range snaps { + // TODO: make JailMode a flag in the snap itself + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, snap.Developer, NotesFromLocal(snap)) + } + + return nil +} + +func tabWriter() *tabwriter.Writer { + return tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) +} diff --git a/cmd/snap/cmd_list_test.go b/cmd/snap/cmd_list_test.go new file mode 100644 index 00000000..4d59685d --- /dev/null +++ b/cmd/snap/cmd_list_test.go @@ -0,0 +1,211 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestListHelp(c *check.C) { + msg := `Usage: + snap.test [OPTIONS] list [list-OPTIONS] [...] + +The list command displays a summary of snaps installed in the current system. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message + +[list command options] + --all Show all revisions +` + rest, err := snap.Parser().ParseArgs([]string{"list", "--help"}) + c.Assert(err.Error(), check.Equals, msg) + c.Assert(rest, check.DeepEquals, []string{}) +} + +func (s *SnapSuite) TestList(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.RawQuery, check.Equals, "") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes +foo +4.2 +17 +bar +- +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestListAll(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.RawQuery, check.Equals, "select=all") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list", "--all"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes +foo +4.2 +17 +bar +- +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestListEmpty(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "No snaps are installed yet. Try \"snap install hello-world\".\n") +} + +func (s *SnapSuite) TestListEmptyWithQuery(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + _, err := snap.Parser().ParseArgs([]string{"list", "quux"}) + c.Assert(err, check.ErrorMatches, `no matching snaps installed`) +} + +func (s *SnapSuite) TestListWithNoMatchingQuery(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.Query().Get("snaps"), check.Equals, "quux") + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + _, err := snap.Parser().ParseArgs([]string{"list", "quux"}) + c.Assert(err, check.ErrorMatches, "no matching snaps installed") +} + +func (s *SnapSuite) TestListWithQuery(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(r.URL.Query().Get("snaps"), check.Equals, "foo") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes +foo +4.2 +17 +bar +- +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestListWithNotes(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintln(w, `{"type": "sync", "result": [ +{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "revision":17, "trymode": true} +,{"name": "dm1", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "devmode"} +,{"name": "dm2", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "strict"} +,{"name": "cf1", "status": "active", "version": "6", "revision":2, "confinement": "devmode", "jailmode": true} +]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?ms)^Name +Version +Rev +Developer +Notes$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^foo +4.2 +17 +bar +try$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^dm1 +.* +devmode$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^dm2 +.* +devmode$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^cf1 +.* +jailmode$`) + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_login.go b/cmd/snap/cmd_login.go new file mode 100644 index 00000000..2d4800ef --- /dev/null +++ b/cmd/snap/cmd_login.go @@ -0,0 +1,129 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "bufio" + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdLogin struct { + Positional struct { + Email string + } `positional-args:"yes"` +} + +var shortLoginHelp = i18n.G("Authenticates on snapd and the store") + +var longLoginHelp = i18n.G(` +The login command authenticates on snapd and the snap store and saves credentials +into the ~/.snap/auth.json file. Further communication with snapd will then be made +using those credentials. + +Login only works for local users in the sudo, admin or wheel groups. + +An account can be setup at https://login.ubuntu.com +`) + +func init() { + addCommand("login", + shortLoginHelp, + longLoginHelp, + func() flags.Commander { + return &cmdLogin{} + }, nil, []argDesc{{ + // TRANSLATORS: This is a noun, and it needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("The login.ubuntu.com email to login as"), + }}) +} + +func requestLoginWith2faRetry(email, password string) error { + var otp []byte + var err error + + var msgs = [3]string{ + i18n.G("Two-factor code: "), + i18n.G("Bad code. Try again: "), + i18n.G("Wrong again. Once more: "), + } + + cli := Client() + reader := bufio.NewReader(nil) + + for i := 0; ; i++ { + // first try is without otp + _, err = cli.Login(email, password, string(otp)) + if i >= len(msgs) || !client.IsTwoFactorError(err) { + return err + } + + reader.Reset(Stdin) + fmt.Fprint(Stdout, msgs[i]) + // the browser shows it as well (and Sergio wants to see it ;) + otp, _, err = reader.ReadLine() + if err != nil { + return err + } + } +} + +func requestLogin(email string) error { + fmt.Fprint(Stdout, fmt.Sprintf(i18n.G("Password of %q: "), email)) + password, err := ReadPassword(0) + fmt.Fprint(Stdout, "\n") + if err != nil { + return err + } + + // strings.TrimSpace needed because we get \r from the pty in the tests + return requestLoginWith2faRetry(email, strings.TrimSpace(string(password))) +} + +func (x *cmdLogin) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + email := x.Positional.Email + if email == "" { + fmt.Fprint(Stdout, i18n.G("Email address: ")) + in, _, err := bufio.NewReader(Stdin).ReadLine() + if err != nil { + return err + } + email = string(in) + } + + err := requestLogin(email) + if err != nil { + return err + } + fmt.Fprintln(Stdout, i18n.G("Login successful")) + + return nil +} diff --git a/cmd/snap/cmd_login_test.go b/cmd/snap/cmd_login_test.go new file mode 100644 index 00000000..c9b43894 --- /dev/null +++ b/cmd/snap/cmd_login_test.go @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +var mockLoginRsp = `{"type": "sync", "result": {"id":42, "username": "foo", "email": "foo@example.com", "macaroon": "yummy", "discarages":"plenty"}}` + +func makeLoginTestServer(c *C, n *int) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + switch *n { + case 0: + c.Check(r.URL.Path, Equals, "/v2/login") + c.Check(r.Method, Equals, "POST") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(string(postData), Equals, `{"email":"foo@example.com","password":"some-password"}`+"\n") + fmt.Fprintln(w, mockLoginRsp) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + *n++ + } +} + +func (s *SnapSuite) TestLoginSimple(c *C) { + n := 0 + s.RedirectClientToTestServer(makeLoginTestServer(c, &n)) + + // send the password + s.password = "some-password\n" + rest, err := snap.Parser().ParseArgs([]string{"login", "foo@example.com"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `Password of "foo@example.com": +Login successful +`) + c.Check(s.Stderr(), Equals, "") + c.Check(n, Equals, 1) +} + +func (s *SnapSuite) TestLoginAskEmail(c *C) { + n := 0 + s.RedirectClientToTestServer(makeLoginTestServer(c, &n)) + + // send the email + fmt.Fprint(s.stdin, "foo@example.com\n") + // send the password + s.password = "some-password" + + rest, err := snap.Parser().ParseArgs([]string{"login"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + // test slightly ugly, on a real system STDOUT will be: + // Email address: foo@example.com\n + // because the input to stdin is echoed + c.Check(s.Stdout(), Equals, `Email address: Password of "foo@example.com": +Login successful +`) + c.Check(s.Stderr(), Equals, "") + c.Check(n, Equals, 1) +} diff --git a/cmd/snap/cmd_logout.go b/cmd/snap/cmd_logout.go new file mode 100644 index 00000000..993012cb --- /dev/null +++ b/cmd/snap/cmd_logout.go @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdLogout struct{} + +var shortLogoutHelp = i18n.G("Log out of the store") + +var longLogoutHelp = i18n.G("This command logs the current user out of the store") + +func init() { + addCommand("logout", + shortLogoutHelp, + longLogoutHelp, + func() flags.Commander { + return &cmdLogout{} + }, nil, nil) +} + +func (cmd *cmdLogout) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return Client().Logout() +} diff --git a/cmd/snap/cmd_managed.go b/cmd/snap/cmd_managed.go new file mode 100644 index 00000000..27cbb944 --- /dev/null +++ b/cmd/snap/cmd_managed.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortIsManagedHelp = i18n.G("Prints whether system is managed") +var longIsManagedHelp = i18n.G(` +The managed command will print true or false informing whether +snapd has registered users. +`) + +type cmdIsManaged struct{} + +func init() { + cmd := addCommand("managed", shortIsManagedHelp, longIsManagedHelp, func() flags.Commander { return &cmdIsManaged{} }, nil, nil) + cmd.hidden = true +} + +func (cmd cmdIsManaged) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + sysinfo, err := Client().SysInfo() + if err != nil { + return err + } + + fmt.Fprintf(Stdout, "%v\n", sysinfo.Managed) + return nil +} diff --git a/cmd/snap/cmd_managed_test.go b/cmd/snap/cmd_managed_test.go new file mode 100644 index 00000000..95014e8d --- /dev/null +++ b/cmd/snap/cmd_managed_test.go @@ -0,0 +1,46 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestManaged(c *C) { + for _, managed := range []bool{true, false} { + s.stdout.Truncate(0) + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/system-info") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {"managed":%v}}`, managed) + }) + + _, err := snap.Parser().ParseArgs([]string{"managed"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, fmt.Sprintf("%v\n", managed)) + } +} diff --git a/cmd/snap/cmd_pack.go b/cmd/snap/cmd_pack.go new file mode 100644 index 00000000..d2e57d2d --- /dev/null +++ b/cmd/snap/cmd_pack.go @@ -0,0 +1,66 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap/pack" +) + +type packCmd struct { + Positional struct { + SnapDir string `positional-arg-name:""` + TargetDir string `positional-arg-name:""` + } `positional-args:"yes"` +} + +var shortPackHelp = i18n.G("pack the given target dir as a snap") +var longPackHelp = i18n.G(` +The pack command packs the given snap-dir as a snap.`) + +func init() { + addCommand("pack", + shortPackHelp, + longPackHelp, + func() flags.Commander { + return &packCmd{} + }, nil, nil) +} + +func (x *packCmd) Execute([]string) error { + if x.Positional.SnapDir == "" { + x.Positional.SnapDir = "." + } + if x.Positional.TargetDir == "" { + x.Positional.TargetDir = "." + } + + snapPath, err := pack.Snap(x.Positional.SnapDir, x.Positional.TargetDir) + if err != nil { + return fmt.Errorf("cannot pack %q: %v", x.Positional.SnapDir, err) + + } + fmt.Fprintf(Stdout, "built: %s\n", snapPath) + return nil +} diff --git a/cmd/snap/cmd_prefer.go b/cmd/snap/cmd_prefer.go new file mode 100644 index 00000000..1a38f290 --- /dev/null +++ b/cmd/snap/cmd_prefer.go @@ -0,0 +1,69 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdPrefer struct { + Positionals struct { + Snap installedSnapName `required:"yes"` + } `positional-args:"true"` +} + +var shortPreferHelp = i18n.G("Prefer aliases from a snap and disable conflicts") +var longPreferHelp = i18n.G(` +The prefer command enables all aliases of the given snap in preference +to conflicting aliases of other snaps whose aliases will be disabled +(removed for manual ones). +`) + +func init() { + addCommand("prefer", shortPreferHelp, longPreferHelp, func() flags.Commander { + return &cmdPrefer{} + }, nil, []argDesc{ + {name: ""}, + }) +} + +func (x *cmdPrefer) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + id, err := cli.Prefer(string(x.Positionals.Snap)) + if err != nil { + return err + } + + chg, err := wait(cli, id) + if err != nil { + return err + } + if err := showAliasChanges(chg); err != nil { + return err + } + + return nil +} diff --git a/cmd/snap/cmd_prefer_test.go b/cmd/snap/cmd_prefer_test.go new file mode 100644 index 00000000..d7dd232c --- /dev/null +++ b/cmd/snap/cmd_prefer_test.go @@ -0,0 +1,75 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestPreferHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] prefer [] + +The prefer command enables all aliases of the given snap in preference +to conflicting aliases of other snaps whose aliases will be disabled +(removed for manual ones). + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"prefer", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestPrefer(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/aliases": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "prefer", + "snap": "some-snap", + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done", "data": {"aliases-added": [{"alias": "alias1", "snap": "some-snap", "app": "cmd1"}]}}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"prefer", "some-snap"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, ""+ + "Added:\n"+ + " - some-snap.cmd1 as alias1\n", + ) + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_prepare_image.go b/cmd/snap/cmd_prepare_image.go new file mode 100644 index 00000000..8519d462 --- /dev/null +++ b/cmd/snap/cmd_prepare_image.go @@ -0,0 +1,77 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "path/filepath" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/image" +) + +type cmdPrepareImage struct { + Positional struct { + ModelAssertionFn string + Rootdir string + } `positional-args:"yes" required:"yes"` + + ExtraSnaps []string `long:"extra-snaps"` + Channel string `long:"channel" default:"stable"` +} + +func init() { + cmd := addCommand("prepare-image", + i18n.G("Prepare a snappy image"), + i18n.G("Prepare a snappy image"), + func() flags.Commander { + return &cmdPrepareImage{} + }, map[string]string{ + "extra-snaps": "Extra snaps to be installed", + "channel": "The channel to use", + }, []argDesc{ + { + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("The model assertion name"), + }, { + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("The output directory"), + }, + }) + cmd.hidden = true +} + +func (x *cmdPrepareImage) Execute(args []string) error { + opts := &image.Options{ + ModelFile: x.Positional.ModelAssertionFn, + + RootDir: filepath.Join(x.Positional.Rootdir, "image"), + GadgetUnpackDir: filepath.Join(x.Positional.Rootdir, "gadget"), + Channel: x.Channel, + Snaps: x.ExtraSnaps, + } + + return image.Prepare(opts) +} diff --git a/cmd/snap/cmd_repair_repairs.go b/cmd/snap/cmd_repair_repairs.go new file mode 100644 index 00000000..cf936c9c --- /dev/null +++ b/cmd/snap/cmd_repair_repairs.go @@ -0,0 +1,93 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/release" +) + +func runSnapRepair(cmdStr string, args []string) error { + // do not even try to run snap-repair on classic, some distros + // may not even package it + if release.OnClassic { + return fmt.Errorf(i18n.G("repairs are not available on a classic system")) + } + + snapRepairPath := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "snap-repair") + args = append([]string{cmdStr}, args...) + cmd := exec.Command(snapRepairPath, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +type cmdShowRepair struct { + Positional struct { + Repair []string `positional-arg-name:""` + } `positional-args:"yes"` +} + +var shortRepairHelp = i18n.G("Shows specific repairs") +var longRepairHelp = i18n.G(` +The repair command shows the details about one or multiple repairs. +`) + +func init() { + cmd := addCommand("repair", shortRepairHelp, longRepairHelp, func() flags.Commander { + return &cmdShowRepair{} + }, nil, nil) + if release.OnClassic { + cmd.hidden = true + } +} + +func (x *cmdShowRepair) Execute(args []string) error { + return runSnapRepair("show", x.Positional.Repair) +} + +type cmdListRepairs struct{} + +var shortRepairsHelp = i18n.G("Lists all repairs") +var longRepairsHelp = i18n.G(` +The repairs command lists all processed repairs for this device. +`) + +func init() { + cmd := addCommand("repairs", shortRepairsHelp, longRepairsHelp, func() flags.Commander { + return &cmdListRepairs{} + }, nil, nil) + if release.OnClassic { + cmd.hidden = true + } +} + +func (x *cmdListRepairs) Execute(args []string) error { + return runSnapRepair("list", args) +} diff --git a/cmd/snap/cmd_repair_repairs_test.go b/cmd/snap/cmd_repair_repairs_test.go new file mode 100644 index 00000000..290806e4 --- /dev/null +++ b/cmd/snap/cmd_repair_repairs_test.go @@ -0,0 +1,67 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/testutil" +) + +func mockSnapRepair(c *C) *testutil.MockCmd { + coreLibExecDir := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir) + err := os.MkdirAll(coreLibExecDir, 0755) + c.Assert(err, IsNil) + return testutil.MockCommand(c, filepath.Join(coreLibExecDir, "snap-repair"), "") +} + +func (s *SnapSuite) TestSnapShowRepair(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockSnapRepair := mockSnapRepair(c) + defer mockSnapRepair.Restore() + + _, err := snap.Parser().ParseArgs([]string{"repair", "canonical-1"}) + c.Assert(err, IsNil) + c.Check(mockSnapRepair.Calls(), DeepEquals, [][]string{ + {"snap-repair", "show", "canonical-1"}, + }) +} + +func (s *SnapSuite) TestSnapListRepairs(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + mockSnapRepair := mockSnapRepair(c) + defer mockSnapRepair.Restore() + + _, err := snap.Parser().ParseArgs([]string{"repairs"}) + c.Assert(err, IsNil) + c.Check(mockSnapRepair.Calls(), DeepEquals, [][]string{ + {"snap-repair", "list"}, + }) +} diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go new file mode 100644 index 00000000..c94de668 --- /dev/null +++ b/cmd/snap/cmd_run.go @@ -0,0 +1,483 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "os" + "os/user" + "path/filepath" + "regexp" + "strconv" + "strings" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapenv" + "github.com/snapcore/snapd/x11" +) + +var ( + syscallExec = syscall.Exec + userCurrent = user.Current + osGetenv = os.Getenv +) + +type cmdRun struct { + Command string `long:"command" hidden:"yes"` + Hook string `long:"hook" hidden:"yes"` + Revision string `short:"r" default:"unset" hidden:"yes"` + Shell bool `long:"shell" ` +} + +func init() { + addCommand("run", + i18n.G("Run the given snap command"), + i18n.G("Run the given snap command with the right confinement and environment"), + func() flags.Commander { + return &cmdRun{} + }, map[string]string{ + "command": i18n.G("Alternative command to run"), + "hook": i18n.G("Hook to run"), + "r": i18n.G("Use a specific snap revision when running hook"), + "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), + }, nil) +} + +func (x *cmdRun) Execute(args []string) error { + if len(args) == 0 { + return fmt.Errorf(i18n.G("need the application to run as argument")) + } + snapApp := args[0] + args = args[1:] + + // Catch some invalid parameter combinations, provide helpful errors + if x.Hook != "" && x.Command != "" { + return fmt.Errorf(i18n.G("cannot use --hook and --command together")) + } + if x.Revision != "unset" && x.Revision != "" && x.Hook == "" { + return fmt.Errorf(i18n.G("-r can only be used with --hook")) + } + if x.Hook != "" && len(args) > 0 { + // TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments + return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.Hook, strings.Join(args, " ")) + } + + // Now actually handle the dispatching + if x.Hook != "" { + return snapRunHook(snapApp, x.Revision, x.Hook) + } + + // pass shell as a special command to snap-exec + if x.Shell { + x.Command = "shell" + } + + if x.Command == "complete" { + snapApp, args = antialias(snapApp, args) + } + + return snapRunApp(snapApp, x.Command, args) +} + +// antialias changes snapApp and args if snapApp is actually an alias +// for something else. If not, or if the args aren't what's expected +// for completion, it returns them unchanged. +func antialias(snapApp string, args []string) (string, []string) { + if len(args) < 7 { + // NOTE if len(args) < 7, Something is Wrong (at least WRT complete.sh and etelpmoc.sh) + return snapApp, args + } + + actualApp, err := resolveApp(snapApp) + if err != nil || actualApp == snapApp { + // no alias! woop. + return snapApp, args + } + + compPoint, err := strconv.Atoi(args[2]) + if err != nil { + // args[2] is not COMP_POINT + return snapApp, args + } + + if compPoint <= len(snapApp) { + // COMP_POINT is inside $0 + return snapApp, args + } + + if compPoint > len(args[5]) { + // COMP_POINT is bigger than $# + return snapApp, args + } + + if args[6] != snapApp { + // args[6] is not COMP_WORDS[0] + return snapApp, args + } + + // it _should_ be COMP_LINE followed by one of + // COMP_WORDBREAKS, but that's hard to do + re, err := regexp.Compile(`^` + regexp.QuoteMeta(snapApp) + `\b`) + if err != nil || !re.MatchString(args[5]) { + // (weird regexp error, or) args[5] is not COMP_LINE + return snapApp, args + } + + argsOut := make([]string, len(args)) + copy(argsOut, args) + + argsOut[2] = strconv.Itoa(compPoint - len(snapApp) + len(actualApp)) + argsOut[5] = re.ReplaceAllLiteralString(args[5], actualApp) + argsOut[6] = actualApp + + return actualApp, argsOut +} + +func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) { + if revision.Unset() { + curFn := filepath.Join(dirs.SnapMountDir, snapName, "current") + realFn, err := os.Readlink(curFn) + if err != nil { + return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err) + } + rev := filepath.Base(realFn) + revision, err = snap.ParseRevision(rev) + if err != nil { + return nil, fmt.Errorf("cannot read revision %s: %s", rev, err) + } + } + + info, err := snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: revision, + }) + if err != nil { + return nil, err + } + + return info, nil +} + +func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { + // 'current' symlink for user data (SNAP_USER_DATA) + userData := info.UserDataDir(usr.HomeDir) + wantedSymlinkValue := filepath.Base(userData) + currentActiveSymlink := filepath.Join(userData, "..", "current") + + var err error + var currentSymlinkValue string + for i := 0; i < 5; i++ { + currentSymlinkValue, err = os.Readlink(currentActiveSymlink) + // Failure other than non-existing symlink is fatal + if err != nil && !os.IsNotExist(err) { + // TRANSLATORS: %v the error message + return fmt.Errorf(i18n.G("cannot read symlink: %v"), err) + } + + if currentSymlinkValue == wantedSymlinkValue { + break + } + + if err == nil { + // We may be racing with other instances of snap-run that try to do the same thing + // If the symlink is already removed then we can ignore this error. + err = os.Remove(currentActiveSymlink) + if err != nil && !os.IsNotExist(err) { + // abort with error + break + } + } + + err = os.Symlink(wantedSymlinkValue, currentActiveSymlink) + // Error other than symlink already exists will abort and be propagated + if err == nil || !os.IsExist(err) { + break + } + // If we arrived here it means the symlink couldn't be created because it got created + // in the meantime by another instance, so we will try again. + } + if err != nil { + return fmt.Errorf(i18n.G("cannot update the 'current' symlink of %q: %v"), currentActiveSymlink, err) + } + return nil +} + +func createUserDataDirs(info *snap.Info) error { + usr, err := userCurrent() + if err != nil { + return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) + } + + // see snapenv.User + userData := info.UserDataDir(usr.HomeDir) + commonUserData := info.UserCommonDataDir(usr.HomeDir) + for _, d := range []string{userData, commonUserData} { + if err := os.MkdirAll(d, 0755); err != nil { + // TRANSLATORS: %q is the directory whose creation failed, %v the error message + return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err) + } + } + + return createOrUpdateUserDataSymlink(info, usr) +} + +func snapRunApp(snapApp, command string, args []string) error { + snapName, appName := snap.SplitSnapApp(snapApp) + info, err := getSnapInfo(snapName, snap.R(0)) + if err != nil { + return err + } + + app := info.Apps[appName] + if app == nil { + return fmt.Errorf(i18n.G("cannot find app %q in %q"), appName, snapName) + } + + return runSnapConfine(info, app.SecurityTag(), snapApp, command, "", args) +} + +func snapRunHook(snapName, snapRevision, hookName string) error { + revision, err := snap.ParseRevision(snapRevision) + if err != nil { + return err + } + + info, err := getSnapInfo(snapName, revision) + if err != nil { + return err + } + + hook := info.Hooks[hookName] + if hook == nil { + return fmt.Errorf(i18n.G("cannot find hook %q in %q"), hookName, snapName) + } + + return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil) +} + +var osReadlink = os.Readlink + +func isReexeced() bool { + exe, err := osReadlink("/proc/self/exe") + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v", err) + return false + } + return strings.HasPrefix(exe, dirs.SnapMountDir) +} + +func migrateXauthority(info *snap.Info) (string, error) { + u, err := userCurrent() + if err != nil { + return "", fmt.Errorf(i18n.G("cannot get the current user: %s"), err) + } + + // If our target directory (XDG_RUNTIME_DIR) doesn't exist we + // don't attempt to create it. + baseTargetDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) + if !osutil.FileExists(baseTargetDir) { + return "", nil + } + + xauthPath := osGetenv("XAUTHORITY") + if len(xauthPath) == 0 || !osutil.FileExists(xauthPath) { + // Nothing to do for us. Most likely running outside of any + // graphical X11 session. + return "", nil + } + + fin, err := os.Open(xauthPath) + if err != nil { + return "", err + } + defer fin.Close() + + // Abs() also calls Clean(); see https://golang.org/pkg/path/filepath/#Abs + xauthPathAbs, err := filepath.Abs(fin.Name()) + if err != nil { + return "", nil + } + + // Remove all symlinks from path + xauthPathCan, err := filepath.EvalSymlinks(xauthPathAbs) + if err != nil { + return "", nil + } + + // Ensure the XAUTHORITY env is not abused by checking that + // it point to exactly the file we just opened (no symlinks, + // no funny "../.." etc) + if fin.Name() != xauthPathCan { + logger.Noticef("WARNING: XAUTHORITY environment value is not a clean path: %q", xauthPathCan) + return "", nil + } + + // Only do the migration from /tmp since the real /tmp is not visible for snaps + if !strings.HasPrefix(fin.Name(), "/tmp/") { + return "", nil + } + + // We are performing a Stat() here to make sure that the user can't + // steal another user's Xauthority file. Note that while Stat() uses + // fstat() on the file descriptor created during Open(), the file might + // have changed ownership between the Open() and the Stat(). That's ok + // because we aren't trying to block access that the user already has: + // if the user has the privileges to chown another user's Xauthority + // file, we won't block that since the user can just steal it without + // having to use snap run. This code is just to ensure that a user who + // doesn't have those privileges can't steal the file via snap run + // (also note that the (potentially untrusted) snap isn't running yet). + fi, err := fin.Stat() + if err != nil { + return "", err + } + sys := fi.Sys() + if sys == nil { + return "", fmt.Errorf(i18n.G("cannot validate owner of file %s"), fin.Name()) + } + // cheap comparison as the current uid is only available as a string + // but it is better to convert the uid from the stat result to a + // string than a string into a number. + if fmt.Sprintf("%d", sys.(*syscall.Stat_t).Uid) != u.Uid { + return "", fmt.Errorf(i18n.G("Xauthority file isn't owned by the current user %s"), u.Uid) + } + + targetPath := filepath.Join(baseTargetDir, ".Xauthority") + + // Only validate Xauthority file again when both files don't match + // otherwise we can continue using the existing Xauthority file. + // This is ok to do here because we aren't trying to protect against + // the user changing the Xauthority file in XDG_RUNTIME_DIR outside + // of snapd. + if osutil.FileExists(targetPath) { + var fout *os.File + if fout, err = os.Open(targetPath); err != nil { + return "", err + } + if osutil.StreamsEqual(fin, fout) { + fout.Close() + return targetPath, nil + } + + fout.Close() + if err := os.Remove(targetPath); err != nil { + return "", err + } + + // Ensure we're validating the Xauthority file from the beginning + if _, err := fin.Seek(int64(os.SEEK_SET), 0); err != nil { + return "", err + } + } + + // To guard against setting XAUTHORITY to non-xauth files, check + // that we have a valid Xauthority. Specifically, the file must be + // parseable as an Xauthority file and not be empty. + if err := x11.ValidateXauthority(fin); err != nil { + return "", err + } + + // Read data from the beginning of the file + if _, err = fin.Seek(int64(os.SEEK_SET), 0); err != nil { + return "", err + } + + fout, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", err + } + defer fout.Close() + + // Read and write validated Xauthority file to its right location + if _, err = io.Copy(fout, fin); err != nil { + if err := os.Remove(targetPath); err != nil { + logger.Noticef("WARNING: cannot remove file at %s: %s", targetPath, err) + } + return "", fmt.Errorf(i18n.G("cannot write new Xauthority file at %s: %s"), targetPath, err) + } + + return targetPath, nil +} + +func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string, args []string) error { + snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine") + // if we re-exec, we must run the snap-confine from the core snap + // as well, if they get out of sync, havoc will happen + if isReexeced() { + // run snap-confine from the core snap. that will work because + // snap-confine on the core snap is mostly statically linked + // (except libudev and libc) + snapConfine = filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, "snap-confine") + } + + if !osutil.FileExists(snapConfine) { + if hook != "" { + logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.Name()) + return nil + } + return fmt.Errorf(i18n.G("missing snap-confine: try updating your snapd package")) + } + + if err := createUserDataDirs(info); err != nil { + logger.Noticef("WARNING: cannot create user data directory: %s", err) + } + + xauthPath, err := migrateXauthority(info) + if err != nil { + logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err) + } + + cmd := []string{snapConfine} + if info.NeedsClassic() { + cmd = append(cmd, "--classic") + } + if info.Base != "" { + cmd = append(cmd, "--base", info.Base) + } + cmd = append(cmd, securityTag) + cmd = append(cmd, filepath.Join(dirs.CoreLibExecDir, "snap-exec")) + + if command != "" { + cmd = append(cmd, "--command="+command) + } + + if hook != "" { + cmd = append(cmd, "--hook="+hook) + } + + // snap-exec is POSIXly-- options must come before positionals. + cmd = append(cmd, snapApp) + cmd = append(cmd, args...) + + extraEnv := make(map[string]string) + if len(xauthPath) > 0 { + extraEnv["XAUTHORITY"] = xauthPath + } + env := snapenv.ExecEnv(info, extraEnv) + + return syscallExec(cmd[0], cmd, env) +} diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go new file mode 100644 index 00000000..b90308ab --- /dev/null +++ b/cmd/snap/cmd_run_test.go @@ -0,0 +1,613 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "gopkg.in/check.v1" + + snaprun "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/x11" +) + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app +hooks: + configure: +`) +var mockContents = "SNAP" + +func (s *SnapSuite) TestInvalidParameters(c *check.C) { + invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "snap-name"} + _, err := snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*cannot use --hook and --command together.*") + + invalidParameters = []string{"run", "-r=1", "--command=command-name", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "-r=1", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "--hook=configure", "foo", "bar", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*") +} + +func (s *SnapSuite) TestSnapRunWhenMissingConfine(c *check.C) { + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + var execs [][]string + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execs = append(execs, args) + return nil + }) + defer restorer() + + // and run it! + // a regular run will fail + _, err = snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, `.* your snapd package`) + // a hook run will not fail + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"}) + c.Assert(err, check.IsNil) + + // but nothing is run ever + c.Check(execs, check.IsNil) +} + +func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml)+"confinement: classic\n", string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "--classic", + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + err = snaprun.SnapRunApp("snapname.app", "my-command", []string{"arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--command=my-command", "snapname.app", "arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, check.IsNil) + info.SideInfo.Revision = snap.R(42) + + fakeHome := c.MkDir() + restorer := snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + err = snaprun.CreateUserDataDirs(info) + c.Assert(err, check.IsNil) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true) +} + +func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Run a hook from the active revision + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Specifically pass "unset" which would use the active version. + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=unset", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + // Create both revisions 41 and 42 + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(41), + }) + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Run a hook on revision 41 + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41") +} + +func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { + // Only create revision 42 + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + return nil + }) + defer restorer() + + // Attempt to run a hook on revision 41, which doesn't exist + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "cannot find .*") +} + +func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "snapname"}) + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"") +} + +func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) { + // Only create revision 42 + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + called := false + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + called = true + return nil + }) + defer restorer() + + err = snaprun.SnapRunHook("snapname", "unset", "missing-hook") + c.Assert(err, check.ErrorMatches, `cannot find hook "missing-hook" in "snapname"`) + c.Check(called, check.Equals, false) +} + +func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--unknown", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, "unknown flag `unknown'") +} + +func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--command=shell"}) + c.Assert(err, check.ErrorMatches, "need the application to run as argument") +} + +func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "not-there"}) + c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir)) +} + +func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execEnv = envv + return nil + }) + defer restorer() + + // set a SNAP{,_*} variable in the environment + os.Setenv("SNAP_NAME", "something-else") + os.Setenv("SNAP_ARCH", "PDP-7") + defer os.Unsetenv("SNAP_NAME") + defer os.Unsetenv("SNAP_ARCH") + // but unrelated stuff is ok + os.Setenv("SNAP_THE_WORLD", "YES") + defer os.Unsetenv("SNAP_THE_WORLD") + + // and ensure those SNAP_ vars get overridden + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") + c.Check(execEnv, check.Not(testutil.Contains), "SNAP_NAME=something-else") + c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7") + c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES") +} + +func (s *SnapSuite) TestSnapRunIsReexeced(c *check.C) { + var osReadlinkResult string + restore := snaprun.MockOsReadlink(func(name string) (string, error) { + return osReadlinkResult, nil + }) + defer restore() + + for _, t := range []struct { + readlink string + expected bool + }{ + {filepath.Join(dirs.SnapMountDir, dirs.CoreLibExecDir, "snapd"), true}, + {filepath.Join(dirs.DistroLibExecDir, "snapd"), false}, + } { + osReadlinkResult = t.readlink + c.Check(snaprun.IsReexeced(), check.Equals, t.expected) + } +} + +func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) { + defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "current", dirs.CoreLibExecDir))() + + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // pretend to be running from core + restorer := snaprun.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil + }) + defer restorer() + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/current", dirs.CoreLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.SnapMountDir, "/core/current", dirs.CoreLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunXauthorityMigration(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + u, err := user.Current() + c.Assert(err, check.IsNil) + + // Ensure XDG_RUNTIME_DIR exists for the user we're testing with + err = os.MkdirAll(filepath.Join(dirs.XdgRuntimeDirBase, u.Uid), 0700) + c.Assert(err, check.IsNil) + + // mock installed snap; happily this also gives us a directory + // below /tmp which the Xauthority migration expects. + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err = os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + xauthPath, err := x11.MockXauthority(2) + c.Assert(err, check.IsNil) + defer os.Remove(xauthPath) + + defer snaprun.MockGetEnv(func(name string) string { + if name == "XAUTHORITY" { + return xauthPath + } + return "" + })() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app"}) + + expectedXauthPath := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid, ".Xauthority") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("XAUTHORITY=%s", expectedXauthPath)) + + info, err := os.Stat(expectedXauthPath) + c.Assert(err, check.IsNil) + c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0600)) + + err = x11.ValidateXauthorityFile(expectedXauthPath) + c.Assert(err, check.IsNil) +} + +// build the args for a hypothetical completer +func mkCompArgs(compPoint string, argv ...string) []string { + out := []string{ + "99", // COMP_TYPE + "99", // COMP_KEY + "", // COMP_POINT + "2", // COMP_CWORD + " ", // COMP_WORDBREAKS + } + out[2] = compPoint + out = append(out, strings.Join(argv, " ")) + out = append(out, argv...) + return out +} + +func (s *SnapSuite) TestAntialiasHappy(c *check.C) { + c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil) + + inArgs := mkCompArgs("10", "alias", "alias", "bo-alias") + + // first not so happy because no alias symlink + app, outArgs := snaprun.Antialias("alias", inArgs) + c.Check(app, check.Equals, "alias") + c.Check(outArgs, check.DeepEquals, inArgs) + + c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil) + + // now really happy + app, outArgs = snaprun.Antialias("alias", inArgs) + c.Check(app, check.Equals, "an-app") + c.Check(outArgs, check.DeepEquals, []string{ + "99", // COMP_TYPE (no change) + "99", // COMP_KEY (no change) + "11", // COMP_POINT (+1 because "an-app" is one longer than "alias") + "2", // COMP_CWORD (no change) + " ", // COMP_WORDBREAKS (no change) + "an-app alias bo-alias", // COMP_LINE (argv[0] changed) + "an-app", // argv (arv[0] changed) + "alias", + "bo-alias", + }) +} + +func (s *SnapSuite) TestAntialiasBailsIfUnhappy(c *check.C) { + // alias exists but args are somehow wonky + c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil) + c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil) + + // weird1 has COMP_LINE not start with COMP_WORDS[0], argv[0] equal to COMP_WORDS[0] + weird1 := mkCompArgs("6", "alias", "") + weird1[5] = "xxxxx " + // weird2 has COMP_LINE not start with COMP_WORDS[0], argv[0] equal to the first word in COMP_LINE + weird2 := mkCompArgs("6", "xxxxx", "") + weird2[5] = "alias " + + for desc, inArgs := range map[string][]string{ + "nil args": nil, + "too-short args": {"alias"}, + "COMP_POINT not a number": mkCompArgs("hello", "alias"), + "COMP_POINT is inside argv[0]": mkCompArgs("2", "alias", ""), + "COMP_POINT is outside argv": mkCompArgs("99", "alias", ""), + "COMP_WORDS[0] is not argv[0]": mkCompArgs("10", "not-alias", ""), + "mismatch between argv[0], COMP_LINE and COMP_WORDS, #1": weird1, + "mismatch between argv[0], COMP_LINE and COMP_WORDS, #2": weird2, + } { + // antialias leaves args alone if it's too short + app, outArgs := snaprun.Antialias("alias", inArgs) + c.Check(app, check.Equals, "alias", check.Commentf(desc)) + c.Check(outArgs, check.DeepEquals, inArgs, check.Commentf(desc)) + } +} diff --git a/cmd/snap/cmd_services.go b/cmd/snap/cmd_services.go new file mode 100644 index 00000000..6526e277 --- /dev/null +++ b/cmd/snap/cmd_services.go @@ -0,0 +1,215 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "strconv" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type svcStatus struct { + Positional struct { + ServiceNames []serviceName `positional-arg-name:""` + } `positional-args:"yes"` +} + +type svcLogs struct { + N string `short:"n" default:"10"` + Follow bool `short:"f"` + Positional struct { + ServiceNames []serviceName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +var ( + shortServicesHelp = i18n.G("Query the status of services") + shortLogsHelp = i18n.G("Retrieve logs of services") + shortStartHelp = i18n.G("Start services") + shortStopHelp = i18n.G("Stop services") + shortRestartHelp = i18n.G("Restart services") +) + +func init() { + addCommand("services", shortServicesHelp, "", func() flags.Commander { return &svcStatus{} }, nil, nil) + addCommand("logs", shortLogsHelp, "", func() flags.Commander { return &svcLogs{} }, nil, nil) + + addCommand("start", shortStartHelp, "", func() flags.Commander { return &svcStart{} }, nil, nil) + addCommand("stop", shortStopHelp, "", func() flags.Commander { return &svcStop{} }, nil, nil) + addCommand("restart", shortRestartHelp, "", func() flags.Commander { return &svcRestart{} }, nil, nil) +} + +func svcNames(s []serviceName) []string { + svcNames := make([]string, len(s)) + for i, svcName := range s { + svcNames[i] = string(svcName) + } + return svcNames +} + +func (s *svcStatus) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + services, err := Client().Apps(svcNames(s.Positional.ServiceNames), client.AppOptions{Service: true}) + if err != nil { + return err + } + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Snap\tService\tStartup\tCurrent")) + + for _, svc := range services { + startup := i18n.G("disabled") + if svc.Enabled { + startup = i18n.G("enabled") + } + current := i18n.G("inactive") + if svc.Active { + current = i18n.G("active") + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", svc.Snap, svc.Name, startup, current) + } + + return nil +} + +func (s *svcLogs) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + sN := -1 + if s.N != "all" { + n, err := strconv.ParseInt(s.N, 0, 32) + if n < 0 || err != nil { + return fmt.Errorf(i18n.G("invalid argument for flag ‘-n’: expected a non-negative integer argument, or “all”.")) + } + sN = int(n) + } + + logs, err := Client().Logs(svcNames(s.Positional.ServiceNames), client.LogOptions{N: sN, Follow: s.Follow}) + if err != nil { + return err + } + + for log := range logs { + fmt.Fprintln(Stdout, log) + } + + return nil +} + +type svcStart struct { + waitMixin + Positional struct { + ServiceNames []serviceName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` + Enable bool `long:"enable"` +} + +func (s *svcStart) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + cli := Client() + names := svcNames(s.Positional.ServiceNames) + changeID, err := cli.Start(names, client.StartOptions{Enable: s.Enable}) + if err != nil { + return err + } + if _, err := s.wait(cli, changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("Started.\n")) + + return nil +} + +type svcStop struct { + waitMixin + Positional struct { + ServiceNames []serviceName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` + Disable bool `long:"disable"` +} + +func (s *svcStop) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + cli := Client() + names := svcNames(s.Positional.ServiceNames) + changeID, err := cli.Stop(names, client.StopOptions{Disable: s.Disable}) + if err != nil { + return err + } + if _, err := s.wait(cli, changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("Stopped.\n")) + + return nil +} + +type svcRestart struct { + waitMixin + Positional struct { + ServiceNames []serviceName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` + Reload bool `long:"reload"` +} + +func (s *svcRestart) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + cli := Client() + names := svcNames(s.Positional.ServiceNames) + changeID, err := cli.Restart(names, client.RestartOptions{Reload: s.Reload}) + if err != nil { + return err + } + if _, err := s.wait(cli, changeID); err != nil { + if err == noWait { + return nil + } + return err + } + + fmt.Fprintf(Stdout, i18n.G("Restarted.\n")) + + return nil +} diff --git a/cmd/snap/cmd_services_test.go b/cmd/snap/cmd_services_test.go new file mode 100644 index 00000000..e86e3e09 --- /dev/null +++ b/cmd/snap/cmd_services_test.go @@ -0,0 +1,173 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +type appOpSuite struct { + BaseSnapSuite + + restoreAll func() +} + +var _ = check.Suite(&appOpSuite{}) + +func (s *appOpSuite) SetUpTest(c *check.C) { + s.BaseSnapSuite.SetUpTest(c) + + restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond) + restorePollTime := snap.MockPollTime(time.Millisecond) + s.restoreAll = func() { + restoreClientRetry() + restorePollTime() + } +} + +func (s *appOpSuite) TearDownTest(c *check.C) { + s.restoreAll() + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *appOpSuite) expectedBody(op string, names []string, extra []string) map[string]interface{} { + inames := make([]interface{}, len(names)) + for i, name := range names { + inames[i] = name + } + expectedBody := map[string]interface{}{ + "action": op, + "names": inames, + } + for _, x := range extra { + expectedBody[x] = true + } + return expectedBody +} + +func (s *appOpSuite) args(op string, names []string, extra []string, noWait bool) []string { + args := []string{op} + if noWait { + args = append(args, "--no-wait") + } + for _, x := range extra { + args = append(args, "--"+x) + } + for _, name := range names { + args = append(args, name) + } + return args +} + +func (s *appOpSuite) testOpNoArgs(c *check.C, op string) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{op}) + c.Assert(err, check.ErrorMatches, `.* required argument .* not provided`) +} + +func (s *appOpSuite) testOpErrorResponse(c *check.C, op string, names []string, extra []string, noWait bool) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/apps") + c.Check(r.URL.Query(), check.HasLen, 0) + c.Check(DecodedRequestBody(c, r), check.DeepEquals, s.expectedBody(op, names, extra)) + w.WriteHeader(400) + fmt.Fprintln(w, `{"type": "error", "result": {"message": "error"}, "status-code": 400}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + + _, err := snap.Parser().ParseArgs(s.args(op, names, extra, noWait)) + c.Assert(err, check.ErrorMatches, "error") + c.Check(n, check.Equals, 1) +} + +func (s *appOpSuite) testOp(c *check.C, op, summary string, names []string, extra []string, noWait bool) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/apps") + c.Check(r.URL.Query(), check.HasLen, 0) + c.Check(DecodedRequestBody(c, r), check.DeepEquals, s.expectedBody(op, names, extra)) + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + case 2: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) + default: + c.Fatalf("expected to get 2 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs(s.args(op, names, extra, noWait)) + c.Assert(err, check.IsNil) + c.Assert(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + expectedN := 3 + if noWait { + summary = "42" + expectedN = 1 + } + c.Check(s.Stdout(), check.Equals, summary+"\n") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, expectedN) +} + +func (s *appOpSuite) TestAppOps(c *check.C) { + extras := []string{"enable", "disable", "reload"} + summaries := []string{"Started.", "Stopped.", "Restarted."} + for i, op := range []string{"start", "stop", "restart"} { + s.testOpNoArgs(c, op) + for _, extra := range [][]string{nil, {extras[i]}} { + for _, noWait := range []bool{false, true} { + for _, names := range [][]string{ + {"foo"}, + {"foo", "bar"}, + {"foo", "bar.baz"}, + } { + s.testOpErrorResponse(c, op, names, extra, noWait) + s.testOp(c, op, summaries[i], names, extra, noWait) + s.stdout.Reset() + } + } + } + } +} diff --git a/cmd/snap/cmd_set.go b/cmd/snap/cmd_set.go new file mode 100644 index 00000000..f1589f77 --- /dev/null +++ b/cmd/snap/cmd_set.go @@ -0,0 +1,96 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/jsonutil" +) + +var shortSetHelp = i18n.G("Changes configuration options") +var longSetHelp = i18n.G(` +The set command changes the provided configuration options as requested. + + $ snap set snap-name username=frank password=$PASSWORD + +All configuration changes are persisted at once, and only after the +snap's configuration hook returns successfully. + +Nested values may be modified via a dotted path: + + $ snap set author.name=frank +`) + +type cmdSet struct { + Positional struct { + Snap installedSnapName + ConfValues []string `required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func init() { + addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} }, nil, []argDesc{ + { + name: "", + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("The snap to configure (e.g. hello-world)"), + }, { + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Configuration value (key=value)"), + }, + }) +} + +func (x *cmdSet) Execute(args []string) error { + patchValues := make(map[string]interface{}) + for _, patchValue := range x.Positional.ConfValues { + parts := strings.SplitN(patchValue, "=", 2) + if len(parts) != 2 { + return fmt.Errorf(i18n.G("invalid configuration: %q (want key=value)"), patchValue) + } + var value interface{} + if err := jsonutil.DecodeWithNumber(strings.NewReader(parts[1]), &value); err != nil { + // Not valid JSON-- just save the string as-is. + patchValues[parts[0]] = parts[1] + } else { + patchValues[parts[0]] = value + } + } + + return configure(string(x.Positional.Snap), patchValues) +} + +func configure(snapName string, patchValues map[string]interface{}) error { + cli := Client() + id, err := cli.SetConf(snapName, patchValues) + if err != nil { + return err + } + + _, err = wait(cli, id) + return err +} diff --git a/cmd/snap/cmd_set_test.go b/cmd/snap/cmd_set_test.go new file mode 100644 index 00000000..fe1c4088 --- /dev/null +++ b/cmd/snap/cmd_set_test.go @@ -0,0 +1,118 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snapset "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" +) + +var validApplyYaml = []byte(`name: snapname +version: 1.0 +hooks: + configure: +`) +var validApplyContents = "" + +func (s *SnapSuite) TestInvalidSetParameters(c *check.C) { + invalidParameters := []string{"set", "snap-name", "key", "value"} + _, err := snapset.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*invalid configuration:.*(want key=value).*") +} + +func (s *SnapSuite) TestSnapSetIntegrationString(c *check.C) { + // mock installed snap + snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, "value") + + // Set a config value for the active snap + _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=value"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestSnapSetIntegrationNumber(c *check.C) { + // mock installed snap + snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, json.Number("1.2")) + + // Set a config value for the active snap + _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=1.2"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestSnapSetIntegrationBigInt(c *check.C) { + snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, json.Number("1234567890")) + + // Set a config value for the active snap + _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", "key=1234567890"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) TestSnapSetIntegrationJson(c *check.C) { + // mock installed snap + snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, map[string]interface{}{"subkey": "value"}) + + // Set a config value for the active snap + _, err := snapset.Parser().ParseArgs([]string{"set", "snapname", `key={"subkey":"value"}`}) + c.Assert(err, check.IsNil) +} + +func (s *SnapSuite) mockSetConfigServer(c *check.C, expectedValue interface{}) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/snaps/snapname/conf": + c.Check(r.Method, check.Equals, "PUT") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "key": expectedValue, + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, check.Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) +} diff --git a/cmd/snap/cmd_shell.go b/cmd/snap/cmd_shell.go new file mode 100644 index 00000000..57105c66 --- /dev/null +++ b/cmd/snap/cmd_shell.go @@ -0,0 +1,100 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "syscall" + + //"github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" +) + +type cmdShell struct { + Positional struct { + ShellType string + } `positional-args:"yes"` +} + +// FIXME: reenable for GA +/* +func init() { + addCommand("shell", + i18n.G("Run snappy shell interface"), + i18n.G("Run snappy shell interface"), + func() flags.Commander { + return &cmdShell{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("The type of shell you want"), + }}) +} +*/ + +// reexec will reexec itself with sudo +func reexecWithSudo() error { + args := []string{"/usr/bin/sudo"} + args = append(args, os.Args...) + env := os.Environ() + if err := syscall.Exec(args[0], args, env); err != nil { + return fmt.Errorf("failed to exec classic shell: %s", err) + } + panic("this should never be reached") +} + +func (x *cmdShell) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + shellType := x.Positional.ShellType + + // FIXME: make this generic so that all snaps can provide a + // shell + if shellType == "classic" { + if !osutil.FileExists("/snap/classic/current") { + return fmt.Errorf(i18n.G(`Classic dimension disabled on this system. +Use "sudo snap install --devmode classic && sudo classic.create" to enable it.`)) + } + + // we need to re-exec if we do not run as root + if os.Getuid() != 0 { + if err := reexecWithSudo(); err != nil { + return err + } + } + + fmt.Fprintln(Stdout, i18n.G(`Entering classic dimension`)) + fmt.Fprintln(Stdout, i18n.G(` + +The home directory is shared between snappy and the classic dimension. +Run "exit" to leave the classic shell. +`)) + args := []string{"/snap/bin/classic.shell"} + return syscall.Exec(args[0], args, os.Environ()) + } + + return fmt.Errorf(i18n.G("unsupported shell %v"), shellType) +} diff --git a/cmd/snap/cmd_sign.go b/cmd/snap/cmd_sign.go new file mode 100644 index 00000000..4bf710bf --- /dev/null +++ b/cmd/snap/cmd_sign.go @@ -0,0 +1,79 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" + "github.com/snapcore/snapd/i18n" +) + +var shortSignHelp = i18n.G("Sign an assertion") +var longSignHelp = i18n.G(`Sign an assertion using the specified key, using the input for headers from a JSON mapping provided through stdin, the body of the assertion can be specified through a "body" pseudo-header. +`) + +type cmdSign struct { + KeyName keyName `short:"k" default:"default"` +} + +func init() { + cmd := addCommand("sign", shortSignHelp, longSignHelp, func() flags.Commander { + return &cmdSign{} + }, map[string]string{"k": i18n.G("Name of the key to use, otherwise use the default key")}, nil) + cmd.hidden = true +} + +func (x *cmdSign) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + statement, err := ioutil.ReadAll(Stdin) + if err != nil { + return fmt.Errorf(i18n.G("cannot read assertion input: %v"), err) + } + + keypairMgr := asserts.NewGPGKeypairManager() + privKey, err := keypairMgr.GetByName(string(x.KeyName)) + if err != nil { + return err + } + + signOpts := signtool.Options{ + KeyID: privKey.PublicKey().ID(), + Statement: statement, + } + + encodedAssert, err := signtool.Sign(&signOpts, keypairMgr) + if err != nil { + return err + } + + _, err = Stdout.Write(encodedAssert) + if err != nil { + return err + } + return nil +} diff --git a/cmd/snap/cmd_sign_build.go b/cmd/snap/cmd_sign_build.go new file mode 100644 index 00000000..888d61e3 --- /dev/null +++ b/cmd/snap/cmd_sign_build.go @@ -0,0 +1,117 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "time" + + _ "golang.org/x/crypto/sha3" // expected for digests + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +type cmdSignBuild struct { + Positional struct { + Filename string + } `positional-args:"yes" required:"yes"` + + // XXX complete DeveloperID and SnapID + DeveloperID string `long:"developer-id" required:"yes"` + SnapID string `long:"snap-id" required:"yes"` + KeyName keyName `short:"k" default:"default" ` + Grade string `long:"grade" choice:"devel" choice:"stable" default:"stable"` +} + +var shortSignBuildHelp = i18n.G("Create snap build assertion") +var longSignBuildHelp = i18n.G("Create snap-build assertion for the provided snap file.") + +func init() { + cmd := addCommand("sign-build", + shortSignBuildHelp, + longSignBuildHelp, + func() flags.Commander { + return &cmdSignBuild{} + }, map[string]string{ + "developer-id": i18n.G("Identifier of the signer"), + "snap-id": i18n.G("Identifier of the snap package associated with the build"), + "k": i18n.G("Name of the GnuPG key to use (defaults to 'default' as key name)"), + "grade": i18n.G("Grade states the build quality of the snap (defaults to 'stable')"), + }, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Filename of the snap you want to assert a build for"), + }}) + cmd.hidden = true +} + +func (x *cmdSignBuild) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapDigest, snapSize, err := asserts.SnapFileSHA3_384(x.Positional.Filename) + if err != nil { + return err + } + + gkm := asserts.NewGPGKeypairManager() + privKey, err := gkm.GetByName(string(x.KeyName)) + if err != nil { + // TRANSLATORS: %q is the key name, %v the error message + return fmt.Errorf(i18n.G("cannot use %q key: %v"), x.KeyName, err) + } + + pubKey := privKey.PublicKey() + timestamp := time.Now().Format(time.RFC3339) + + headers := map[string]interface{}{ + "developer-id": x.DeveloperID, + "authority-id": x.DeveloperID, + "snap-sha3-384": snapDigest, + "snap-id": x.SnapID, + "snap-size": fmt.Sprintf("%d", snapSize), + "grade": x.Grade, + "timestamp": timestamp, + } + + adb, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + KeypairManager: gkm, + }) + if err != nil { + return fmt.Errorf(i18n.G("cannot open the assertions database: %v"), err) + } + + a, err := adb.Sign(asserts.SnapBuildType, headers, nil, pubKey.ID()) + if err != nil { + return fmt.Errorf(i18n.G("cannot sign assertion: %v"), err) + } + + _, err = Stdout.Write(asserts.Encode(a)) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/snap/cmd_sign_build_test.go b/cmd/snap/cmd_sign_build_test.go new file mode 100644 index 00000000..09ea2744 --- /dev/null +++ b/cmd/snap/cmd_sign_build_test.go @@ -0,0 +1,134 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + snap "github.com/snapcore/snapd/cmd/snap" +) + +type SnapSignBuildSuite struct { + BaseSnapSuite +} + +var _ = Suite(&SnapSignBuildSuite{}) + +func (s *SnapSignBuildSuite) TestSignBuildMandatoryFlags(c *C) { + _, err := snap.Parser().ParseArgs([]string{"sign-build", "foo_1_amd64.snap"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "the required flags `--developer-id' and `--snap-id' were not specified") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSignBuildSuite) TestSignBuildMissingSnap(c *C) { + _, err := snap.Parser().ParseArgs([]string{"sign-build", "foo_1_amd64.snap", "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "cannot compute snap \"foo_1_amd64.snap\" digest: open foo_1_amd64.snap: no such file or directory") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSignBuildSuite) TestSignBuildMissingKey(c *C) { + snapFilename := "foo_1_amd64.snap" + _err := ioutil.WriteFile(snapFilename, []byte("sample"), 0644) + c.Assert(_err, IsNil) + defer os.Remove(snapFilename) + + tempdir := c.MkDir() + os.Setenv("SNAP_GNUPG_HOME", tempdir) + defer os.Unsetenv("SNAP_GNUPG_HOME") + + _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, "cannot use \"default\" key: cannot find key named \"default\" in GPG keyring") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSignBuildSuite) TestSignBuildWorks(c *C) { + snapFilename := "foo_1_amd64.snap" + snapContent := []byte("sample") + _err := ioutil.WriteFile(snapFilename, snapContent, 0644) + c.Assert(_err, IsNil) + defer os.Remove(snapFilename) + + tempdir := c.MkDir() + for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { + data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644) + c.Assert(err, IsNil) + } + os.Setenv("SNAP_GNUPG_HOME", tempdir) + defer os.Unsetenv("SNAP_GNUPG_HOME") + + _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1"}) + c.Assert(err, IsNil) + + assertion, err := asserts.Decode([]byte(s.Stdout())) + c.Assert(err, IsNil) + c.Check(assertion.Type(), Equals, asserts.SnapBuildType) + c.Check(assertion.Revision(), Equals, 0) + c.Check(assertion.HeaderString("authority-id"), Equals, "dev-id1") + c.Check(assertion.HeaderString("developer-id"), Equals, "dev-id1") + c.Check(assertion.HeaderString("grade"), Equals, "stable") + c.Check(assertion.HeaderString("snap-id"), Equals, "snap-id-1") + c.Check(assertion.HeaderString("snap-size"), Equals, fmt.Sprintf("%d", len(snapContent))) + c.Check(assertion.HeaderString("snap-sha3-384"), Equals, "jyP7dUgb8HiRNd1SdYPp_il-YNrl6P6PgNAe-j6_7WytjKslENhMD3Of5XBU5bQK") + + // check for valid signature ?! + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSignBuildSuite) TestSignBuildWorksDevelGrade(c *C) { + snapFilename := "foo_1_amd64.snap" + snapContent := []byte("sample") + _err := ioutil.WriteFile(snapFilename, snapContent, 0644) + c.Assert(_err, IsNil) + defer os.Remove(snapFilename) + + tempdir := c.MkDir() + for _, fileName := range []string{"pubring.gpg", "secring.gpg", "trustdb.gpg"} { + data, err := ioutil.ReadFile(filepath.Join("test-data", fileName)) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644) + c.Assert(err, IsNil) + } + os.Setenv("SNAP_GNUPG_HOME", tempdir) + defer os.Unsetenv("SNAP_GNUPG_HOME") + + _, err := snap.Parser().ParseArgs([]string{"sign-build", snapFilename, "--developer-id", "dev-id1", "--snap-id", "snap-id-1", "--grade", "devel"}) + c.Assert(err, IsNil) + assertion, err := asserts.Decode([]byte(s.Stdout())) + c.Assert(err, IsNil) + c.Check(assertion.Type(), Equals, asserts.SnapBuildType) + c.Check(assertion.HeaderString("grade"), Equals, "devel") + + // check for valid signature ?! + c.Check(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_sign_test.go b/cmd/snap/cmd_sign_test.go new file mode 100644 index 00000000..341551e7 --- /dev/null +++ b/cmd/snap/cmd_sign_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +var statement = []byte(fmt.Sprintf(`{"type": "snap-build", +"authority-id": "devel1", +"series": "16", +"snap-id": "snapidsnapidsnapidsnapidsnapidsn", +"snap-sha3-384": "QlqR0uAWEAWF5Nwnzj5kqmmwFslYPu1IL16MKtLKhwhv0kpBv5wKZ_axf_nf_2cL", +"snap-size": "1", +"grade": "devel", +"timestamp": %q +}`, time.Now().Format(time.RFC3339))) + +func (s *SnapKeysSuite) TestHappyDefaultKey(c *C) { + s.stdin.Write(statement) + + rest, err := snap.Parser().ParseArgs([]string{"sign"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + a, err := asserts.Decode(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapBuildType) +} + +func (s *SnapKeysSuite) TestHappyNonDefaultKey(c *C) { + s.stdin.Write(statement) + + rest, err := snap.Parser().ParseArgs([]string{"sign", "-k", "another"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + a, err := asserts.Decode(s.stdout.Bytes()) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapBuildType) +} diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go new file mode 100644 index 00000000..1b1dc40f --- /dev/null +++ b/cmd/snap/cmd_snap_op.go @@ -0,0 +1,1018 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "errors" + "fmt" + "os" + "os/signal" + "path/filepath" + "sort" + "strings" + "syscall" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/progress" +) + +func lastLogStr(logs []string) string { + if len(logs) == 0 { + return "" + } + return logs[len(logs)-1] +} + +var ( + maxGoneTime = 5 * time.Second + pollTime = 100 * time.Millisecond +) + +type waitMixin struct { + NoWait bool `long:"no-wait" hidden:"true"` +} + +// TODO: use waitMixin outside of cmd_snap_op.go + +var waitDescs = mixinDescs{ + "no-wait": i18n.G("Do not wait for the operation to finish but just print the change id."), +} + +var noWait = errors.New("no wait for op") + +func (wmx *waitMixin) wait(cli *client.Client, id string) (*client.Change, error) { + if wmx.NoWait { + fmt.Fprintf(Stdout, "%s\n", id) + return nil, noWait + } + return wait(cli, id) +} + +func wait(cli *client.Client, id string) (*client.Change, error) { + pb := progress.MakeProgressBar() + defer func() { + pb.Finished() + }() + + tMax := time.Time{} + + var lastID string + lastLog := map[string]string{} + for { + chg, err := cli.Change(id) + if err != nil { + // a client.Error means we were able to communicate with + // the server (got an answer) + if e, ok := err.(*client.Error); ok { + return nil, e + } + + // an non-client error here means the server most + // likely went away + // XXX: it actually can be a bunch of other things; fix client to expose it better + now := time.Now() + if tMax.IsZero() { + tMax = now.Add(maxGoneTime) + } + if now.After(tMax) { + return nil, err + } + pb.Spin(i18n.G("Waiting for server to restart")) + time.Sleep(pollTime) + continue + } + if !tMax.IsZero() { + pb.Finished() + tMax = time.Time{} + } + + for _, t := range chg.Tasks { + switch { + case t.Status != "Doing": + continue + case t.Progress.Total == 1: + pb.Spin(t.Summary) + nowLog := lastLogStr(t.Log) + if lastLog[t.ID] != nowLog { + pb.Notify(nowLog) + lastLog[t.ID] = nowLog + } + case t.ID == lastID: + pb.Set(float64(t.Progress.Done)) + default: + pb.Start(t.Summary, float64(t.Progress.Total)) + lastID = t.ID + } + break + } + + if chg.Ready { + if chg.Status == "Done" { + return chg, nil + } + + if chg.Err != "" { + return chg, errors.New(chg.Err) + } + + return nil, fmt.Errorf(i18n.G("change finished in status %q with no error message"), chg.Status) + } + + // note this very purposely is not a ticker; we want + // to sleep 100ms between calls, not call once every + // 100ms. + time.Sleep(pollTime) + } +} + +var ( + shortInstallHelp = i18n.G("Installs a snap to the system") + shortRemoveHelp = i18n.G("Removes a snap from the system") + shortRefreshHelp = i18n.G("Refreshes a snap in the system") + shortTryHelp = i18n.G("Tests a snap in the system") + shortEnableHelp = i18n.G("Enables a snap in the system") + shortDisableHelp = i18n.G("Disables a snap in the system") +) + +var longInstallHelp = i18n.G(` +The install command installs the named snap in the system. +`) + +var longRemoveHelp = i18n.G(` +The remove command removes the named snap from the system. + +By default all the snap revisions are removed, including their data and the common +data directory. When a --revision option is passed only the specified revision is +removed. +`) + +var longRefreshHelp = i18n.G(` +The refresh command refreshes (updates) the named snap. +`) + +var longTryHelp = i18n.G(` +The try command installs an unpacked snap into the system for testing purposes. +The unpacked snap content continues to be used even after installation, so +non-metadata changes there go live instantly. Metadata changes such as those +performed in snap.yaml will require reinstallation to go live. + +If snap-dir argument is omitted, the try command will attempt to infer it if +either snapcraft.yaml file and prime directory or meta/snap.yaml file can be +found relative to current working directory. +`) + +var longEnableHelp = i18n.G(` +The enable command enables a snap that was previously disabled. +`) + +var longDisableHelp = i18n.G(` +The disable command disables a snap. The binaries and services of the +snap will no longer be available. But all the data is still available +and the snap can easily be enabled again. +`) + +type cmdRemove struct { + waitMixin + + Revision string `long:"revision"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdRemove) removeOne(opts *client.SnapOptions) error { + name := string(x.Positional.Snaps[0]) + + cli := Client() + changeID, err := cli.Remove(name, opts) + if err != nil { + msg, err := errorToCmdMessage(name, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name) + return nil +} + +func (x *cmdRemove) removeMany(opts *client.SnapOptions) error { + names := make([]string, len(x.Positional.Snaps)) + for i, s := range x.Positional.Snaps { + names[i] = string(s) + } + + cli := Client() + changeID, err := cli.RemoveMany(names, opts) + if err != nil { + return err + } + + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + var removed []string + if err := chg.Get("snap-names", &removed); err != nil && err != client.ErrNoData { + return err + } + + seen := make(map[string]bool) + for _, name := range removed { + fmt.Fprintf(Stdout, i18n.G("%s removed\n"), name) + seen[name] = true + } + for _, name := range names { + if !seen[name] { + // FIXME: this is the only reason why a name can be + // skipped, but it does feel awkward + fmt.Fprintf(Stdout, i18n.G("%s not installed\n"), name) + } + } + + return nil + +} + +func (x *cmdRemove) Execute([]string) error { + opts := &client.SnapOptions{Revision: x.Revision} + if len(x.Positional.Snaps) == 1 { + return x.removeOne(opts) + } + + if x.Revision != "" { + return errors.New(i18n.G("a single snap name is needed to specify the revision")) + } + return x.removeMany(nil) +} + +type channelMixin struct { + Channel string `long:"channel"` + + // shortcuts + EdgeChannel bool `long:"edge"` + BetaChannel bool `long:"beta"` + CandidateChannel bool `long:"candidate"` + StableChannel bool `long:"stable" ` +} + +type mixinDescs map[string]string + +func (mxd mixinDescs) also(m map[string]string) mixinDescs { + n := make(map[string]string, len(mxd)+len(m)) + for k, v := range mxd { + n[k] = v + } + for k, v := range m { + n[k] = v + } + return n +} + +var channelDescs = mixinDescs{ + "channel": i18n.G("Use this channel instead of stable"), + "beta": i18n.G("Install from the beta channel"), + "edge": i18n.G("Install from the edge channel"), + "candidate": i18n.G("Install from the candidate channel"), + "stable": i18n.G("Install from the stable channel"), +} + +func (mx *channelMixin) setChannelFromCommandline() error { + for _, ch := range []struct { + enabled bool + chName string + }{ + {mx.StableChannel, "stable"}, + {mx.CandidateChannel, "candidate"}, + {mx.BetaChannel, "beta"}, + {mx.EdgeChannel, "edge"}, + } { + if !ch.enabled { + continue + } + if mx.Channel != "" { + return fmt.Errorf("Please specify a single channel") + } + mx.Channel = ch.chName + } + + if !strings.Contains(mx.Channel, "/") && mx.Channel != "" && mx.Channel != "edge" && mx.Channel != "beta" && mx.Channel != "candidate" && mx.Channel != "stable" { + // shortcut to jump to a different track, e.g. + // snap install foo --channel=3.4 # implies 3.4/stable + mx.Channel += "/stable" + } + + return nil +} + +// show what has been done +func showDone(names []string, op string) error { + cli := Client() + snaps, err := cli.List(names, nil) + if err != nil { + return err + } + + for _, snap := range snaps { + channelStr := "" + if snap.Channel != "" && snap.Channel != "stable" { + channelStr = fmt.Sprintf(" (%s)", snap.Channel) + } + switch op { + case "install": + if snap.Developer != "" { + // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version, then the developer name (e.g. "some-snap (beta) 1.3 from 'alice' installed") + fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' installed\n"), snap.Name, channelStr, snap.Version, snap.Developer) + } else { + // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version (e.g. "some-snap (beta) 1.3 installed") + fmt.Fprintf(Stdout, i18n.G("%s%s %s installed\n"), snap.Name, channelStr, snap.Version) + } + case "refresh": + if snap.Developer != "" { + // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version, then the developer name (e.g. "some-snap (beta) 1.3 from 'alice' refreshed") + fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' refreshed\n"), snap.Name, channelStr, snap.Version, snap.Developer) + } else { + // TRANSLATORS: the args are a snap name optionally followed by a channel, then a version (e.g. "some-snap (beta) 1.3 refreshed") + fmt.Fprintf(Stdout, i18n.G("%s%s %s refreshed\n"), snap.Name, channelStr, snap.Version) + } + case "revert": + // TRANSLATORS: first %s is a snap name, second %s is a revision + fmt.Fprintf(Stdout, i18n.G("%s reverted to %s\n"), snap.Name, snap.Version) + default: + fmt.Fprintf(Stdout, "internal error, unknown op %q", op) + } + if snap.TrackingChannel != snap.Channel { + // TRANSLATORS: first %s is a snap name, following %s is a channel name + fmt.Fprintf(Stdout, i18n.G("This leaves %s tracking %s.\n"), snap.Name, snap.TrackingChannel) + } + } + + return nil +} + +func (mx *channelMixin) asksForChannel() bool { + return mx.Channel != "" +} + +type modeMixin struct { + DevMode bool `long:"devmode"` + JailMode bool `long:"jailmode"` + Classic bool `long:"classic"` +} + +var modeDescs = mixinDescs{ + "classic": i18n.G("Put snap in classic mode and disable security confinement"), + "devmode": i18n.G("Put snap in development mode and disable security confinement"), + "jailmode": i18n.G("Put snap in enforced confinement mode"), +} + +var errModeConflict = errors.New(i18n.G("cannot use devmode and jailmode flags together")) + +func (mx modeMixin) validateMode() error { + if mx.DevMode && mx.JailMode { + return errModeConflict + } + return nil +} + +func (mx modeMixin) asksForMode() bool { + return mx.DevMode || mx.JailMode || mx.Classic +} + +func (mx modeMixin) setModes(opts *client.SnapOptions) { + opts.DevMode = mx.DevMode + opts.JailMode = mx.JailMode + opts.Classic = mx.Classic +} + +type cmdInstall struct { + waitMixin + + channelMixin + modeMixin + Revision string `long:"revision"` + + Dangerous bool `long:"dangerous"` + // alias for --dangerous, deprecated but we need to support it + // because we released 2.14.2 with --force-dangerous + ForceDangerous bool `long:"force-dangerous" hidden:"yes"` + + Unaliased bool `long:"unaliased"` + + Positional struct { + Snaps []remoteSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func setupAbortHandler(changeId string) { + // Intercept sigint + c := make(chan os.Signal, 2) + signal.Notify(c, syscall.SIGINT) + go func() { + <-c + cli := Client() + _, err := cli.Abort(changeId) + if err != nil { + fmt.Fprintf(Stderr, err.Error()+"\n") + } + }() +} + +func (x *cmdInstall) installOne(name string, opts *client.SnapOptions) error { + var err error + var installFromFile bool + var changeID string + + cli := Client() + if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") { + installFromFile = true + changeID, err = cli.InstallPath(name, opts) + } else { + changeID, err = cli.Install(name, opts) + } + if err != nil { + msg, err := errorToCmdMessage(name, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + setupAbortHandler(changeID) + + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + // extract the snapName from the change, important for sideloaded + var snapName string + + if installFromFile { + if err := chg.Get("snap-name", &snapName); err != nil { + return fmt.Errorf("cannot extract the snap-name from local file %q: %s", name, err) + } + name = snapName + } + + return showDone([]string{name}, "install") +} + +func (x *cmdInstall) installMany(names []string, opts *client.SnapOptions) error { + // sanity check + for _, name := range names { + if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") { + return fmt.Errorf("only one snap file can be installed at a time") + } + } + + cli := Client() + changeID, err := cli.InstallMany(names, opts) + if err != nil { + return err + } + + setupAbortHandler(changeID) + + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + var installed []string + if err := chg.Get("snap-names", &installed); err != nil && err != client.ErrNoData { + return err + } + + if len(installed) > 0 { + if err := showDone(installed, "install"); err != nil { + return err + } + } + + // show skipped + seen := make(map[string]bool) + for _, name := range installed { + seen[name] = true + } + for _, name := range names { + if !seen[name] { + // FIXME: this is the only reason why a name can be + // skipped, but it does feel awkward + fmt.Fprintf(Stdout, i18n.G("%s already installed\n"), name) + } + } + + return nil +} + +func (x *cmdInstall) Execute([]string) error { + if err := x.setChannelFromCommandline(); err != nil { + return err + } + if err := x.validateMode(); err != nil { + return err + } + + dangerous := x.Dangerous || x.ForceDangerous + opts := &client.SnapOptions{ + Channel: x.Channel, + Revision: x.Revision, + Dangerous: dangerous, + Unaliased: x.Unaliased, + } + x.setModes(opts) + + names := make([]string, len(x.Positional.Snaps)) + for i, name := range x.Positional.Snaps { + names[i] = string(name) + } + + if len(names) == 1 { + return x.installOne(names[0], opts) + } + + if x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags")) + } + + return x.installMany(names, nil) +} + +type cmdRefresh struct { + waitMixin + + channelMixin + modeMixin + + Revision string `long:"revision"` + List bool `long:"list"` + Time bool `long:"time"` + IgnoreValidation bool `long:"ignore-validation"` + Positional struct { + Snaps []installedSnapName `positional-arg-name:""` + } `positional-args:"yes"` +} + +func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error { + cli := Client() + changeID, err := cli.RefreshMany(snaps, opts) + if err != nil { + return err + } + + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + var refreshed []string + if err := chg.Get("snap-names", &refreshed); err != nil && err != client.ErrNoData { + return err + } + + if len(refreshed) > 0 { + return showDone(refreshed, "refresh") + } + + fmt.Fprintln(Stderr, i18n.G("All snaps up to date.")) + + return nil +} + +func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error { + cli := Client() + changeID, err := cli.Refresh(name, opts) + if err != nil { + msg, err := errorToCmdMessage(name, err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + return showDone([]string{name}, "refresh") +} + +func (x *cmdRefresh) showRefreshTimes() error { + cli := Client() + sysinfo, err := cli.SysInfo() + if err != nil { + return err + } + + fmt.Fprintf(Stdout, "schedule: %s\n", sysinfo.Refresh.Schedule) + if sysinfo.Refresh.Last != "" { + fmt.Fprintf(Stdout, "last: %s\n", sysinfo.Refresh.Last) + } else { + fmt.Fprintf(Stdout, "last: n/a\n") + } + if sysinfo.Refresh.Next != "" { + fmt.Fprintf(Stdout, "next: %s\n", sysinfo.Refresh.Next) + } else { + fmt.Fprintf(Stdout, "next: n/a\n") + } + return nil +} + +func (x *cmdRefresh) listRefresh() error { + cli := Client() + snaps, _, err := cli.Find(&client.FindOptions{ + Refresh: true, + }) + if err != nil { + return err + } + if len(snaps) == 0 { + fmt.Fprintln(Stderr, i18n.G("All snaps up to date.")) + return nil + } + + sort.Sort(snapsByName(snaps)) + + w := tabWriter() + defer w.Flush() + + fmt.Fprintln(w, i18n.G("Name\tVersion\tRev\tDeveloper\tNotes")) + for _, snap := range snaps { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Revision, snap.Developer, NotesFromRemote(snap, nil)) + } + + return nil +} + +func (x *cmdRefresh) Execute([]string) error { + if err := x.setChannelFromCommandline(); err != nil { + return err + } + if err := x.validateMode(); err != nil { + return err + } + + if x.Time { + if x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("--time does not take mode nor channel flags")) + } + + return x.showRefreshTimes() + } + + if x.List { + if x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("--list does not take mode nor channel flags")) + } + + return x.listRefresh() + } + + if len(x.Positional.Snaps) == 0 && os.Getenv("SNAP_REFRESH_FROM_TIMER") == "1" { + fmt.Fprintf(Stdout, "Ignoring `snap refresh` from the systemd timer") + return nil + } + + names := make([]string, len(x.Positional.Snaps)) + for i, name := range x.Positional.Snaps { + names[i] = string(name) + } + if len(x.Positional.Snaps) == 1 { + opts := &client.SnapOptions{ + Channel: x.Channel, + IgnoreValidation: x.IgnoreValidation, + Revision: x.Revision, + } + x.setModes(opts) + return x.refreshOne(names[0], opts) + } + + if x.asksForMode() || x.asksForChannel() { + return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags")) + } + + if x.IgnoreValidation { + return errors.New(i18n.G("a single snap name must be specified when ignoring validation")) + } + + return x.refreshMany(names, nil) +} + +type cmdTry struct { + waitMixin + + modeMixin + Positional struct { + SnapDir string `positional-arg-name:""` + } `positional-args:"yes"` +} + +func hasSnapcraftYaml() bool { + for _, loc := range []string{ + "snap/snapcraft.yaml", + "snapcraft.yaml", + ".snapcraft.yaml", + } { + if osutil.FileExists(loc) { + return true + } + } + + return false +} + +func (x *cmdTry) Execute([]string) error { + if err := x.validateMode(); err != nil { + return err + } + cli := Client() + name := x.Positional.SnapDir + opts := &client.SnapOptions{} + x.setModes(opts) + + if name == "" { + if hasSnapcraftYaml() && osutil.IsDirectory("prime") { + name = "prime" + } else { + if osutil.FileExists("meta/snap.yaml") { + name = "./" + } + } + if name == "" { + return fmt.Errorf(i18n.G("error: the `` argument was not provided and couldn't be inferred")) + } + } + + path, err := filepath.Abs(name) + if err != nil { + // TRANSLATORS: %q gets what the user entered, %v gets the resulting error message + return fmt.Errorf(i18n.G("cannot get full path for %q: %v"), name, err) + } + + changeID, err := cli.Try(path, opts) + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindNotSnap { + return fmt.Errorf(i18n.G(`%q does not contain an unpacked snap. + +Try "snapcraft prime" in your project directory, then "snap try" again.`), path) + } + if err != nil { + return err + } + + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + // extract the snap name + var snapName string + if err := chg.Get("snap-name", &snapName); err != nil { + // TRANSLATORS: %q gets the snap name, %v gets the resulting error message + return fmt.Errorf(i18n.G("cannot extract the snap-name from local file %q: %v"), name, err) + } + name = snapName + + // show output as speced + snaps, err := cli.List([]string{name}, nil) + if err != nil { + return err + } + if len(snaps) != 1 { + // TRANSLATORS: %q gets the snap name, %v the list of things found when trying to list it + return fmt.Errorf(i18n.G("cannot get data for %q: %v"), name, snaps) + } + snap := snaps[0] + // TRANSLATORS: 1. snap name, 2. snap version (keep those together please). the 3rd %s is a path (where it's mounted from). + fmt.Fprintf(Stdout, i18n.G("%s %s mounted from %s\n"), name, snap.Version, path) + return nil +} + +type cmdEnable struct { + waitMixin + + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdEnable) Execute([]string) error { + cli := Client() + name := string(x.Positional.Snap) + opts := &client.SnapOptions{} + changeID, err := cli.Enable(name, opts) + if err != nil { + return err + } + + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s enabled\n"), name) + return nil +} + +type cmdDisable struct { + waitMixin + + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +func (x *cmdDisable) Execute([]string) error { + cli := Client() + name := string(x.Positional.Snap) + opts := &client.SnapOptions{} + changeID, err := cli.Disable(name, opts) + if err != nil { + return err + } + + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("%s disabled\n"), name) + return nil +} + +type cmdRevert struct { + waitMixin + + modeMixin + Revision string `long:"revision"` + Positional struct { + Snap installedSnapName `positional-arg-name:""` + } `positional-args:"yes" required:"yes"` +} + +var shortRevertHelp = i18n.G("Reverts the given snap to the previous state") +var longRevertHelp = i18n.G(` +The revert command reverts the given snap to its state before +the latest refresh. This will reactivate the previous snap revision, +and will use the original data that was associated with that revision, +discarding any data changes that were done by the latest revision. As +an exception, data which the snap explicitly chooses to share across +revisions is not touched by the revert process. +`) + +func (x *cmdRevert) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if err := x.validateMode(); err != nil { + return err + } + + cli := Client() + name := string(x.Positional.Snap) + opts := &client.SnapOptions{Revision: x.Revision} + x.setModes(opts) + changeID, err := cli.Revert(name, opts) + if err != nil { + return err + } + + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { + return err + } + + return showDone([]string{name}, "revert") +} + +var shortSwitchHelp = i18n.G("Switches snap to a different channel") +var longSwitchHelp = i18n.G(` +The switch command switches the given snap to a different channel without +doing a refresh. +`) + +type cmdSwitch struct { + channelMixin + + Positional struct { + Snap installedSnapName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func (x cmdSwitch) Execute(args []string) error { + if err := x.setChannelFromCommandline(); err != nil { + return err + } + if x.Channel == "" { + return fmt.Errorf("missing --channel= parameter") + } + + cli := Client() + name := string(x.Positional.Snap) + channel := string(x.Channel) + opts := &client.SnapOptions{ + Channel: channel, + } + changeID, err := cli.Switch(name, opts) + if err != nil { + return err + } + + if _, err = wait(cli, changeID); err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("%q switched to the %q channel\n"), name, channel) + return nil +} + +func init() { + addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} }, + waitDescs.also(map[string]string{"revision": i18n.G("Remove only the given revision")}), nil) + addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} }, + waitDescs.also(channelDescs).also(modeDescs).also(map[string]string{ + "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"), + "dangerous": i18n.G("Install the given snap file even if there are no pre-acknowledged signatures for it, meaning it was not verified and could be dangerous (--devmode implies this)"), + "force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"), + "unaliased": i18n.G("Install the given snap without enabling its automatic aliases"), + }), nil) + addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, + waitDescs.also(channelDescs).also(modeDescs).also(map[string]string{ + "revision": i18n.G("Refresh to the given revision"), + "list": i18n.G("Show available snaps for refresh but do not perform a refresh"), + "time": i18n.G("Show auto refresh information but do not perform a refresh"), + "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"), + }), nil) + addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, waitDescs.also(modeDescs), nil) + addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, waitDescs, nil) + addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, waitDescs, nil) + addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, waitDescs.also(modeDescs).also(map[string]string{ + "revision": "Revert to the given revision", + }), nil) + addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, nil, nil) + +} diff --git a/cmd/snap/cmd_snap_op_test.go b/cmd/snap/cmd_snap_op_test.go new file mode 100644 index 00000000..824a6412 --- /dev/null +++ b/cmd/snap/cmd_snap_op_test.go @@ -0,0 +1,1044 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "mime" + "mime/multipart" + "net/http" + "net/http/httptest" + "path/filepath" + "regexp" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/progress/progresstest" + "github.com/snapcore/snapd/testutil" +) + +type snapOpTestServer struct { + c *check.C + + checker func(r *http.Request) + n int + total int + channel string +} + +var _ = check.Suite(&SnapOpSuite{}) + +func (t *snapOpTestServer) handle(w http.ResponseWriter, r *http.Request) { + switch t.n { + case 0: + t.checker(r) + t.c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + case 1: + t.c.Check(r.Method, check.Equals, "GET") + t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + case 2: + t.c.Check(r.Method, check.Equals, "GET") + t.c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-name": "foo"}}}`) + case 3: + t.c.Check(r.Method, check.Equals, "GET") + t.c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "1.0", "developer": "bar", "revision":42, "channel":"%s"}]}\n`, t.channel) + default: + t.c.Fatalf("expected to get %d requests, now on %d", t.total, t.n+1) + } + + t.n++ +} + +type SnapOpSuite struct { + BaseSnapSuite + + restoreAll func() + srv snapOpTestServer +} + +func (s *SnapOpSuite) SetUpTest(c *check.C) { + s.BaseSnapSuite.SetUpTest(c) + + restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond) + restorePollTime := snap.MockPollTime(time.Millisecond) + s.restoreAll = func() { + restoreClientRetry() + restorePollTime() + } + + s.srv = snapOpTestServer{ + c: c, + total: 4, + } +} + +func (s *SnapOpSuite) TearDownTest(c *check.C) { + s.restoreAll() + s.BaseSnapSuite.TearDownTest(c) +} + +func (s *SnapOpSuite) TestWait(c *check.C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + // lazy way of getting a URL that won't work nor break stuff + server := httptest.NewServer(nil) + snap.ClientConfig.BaseURL = server.URL + server.Close() + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + c.Assert(chg, check.IsNil) + c.Assert(err, check.NotNil) + c.Check(meter.Labels, testutil.Contains, "Waiting for server to restart") +} + +func (s *SnapOpSuite) TestWaitRecovers(c *check.C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + nah := true + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + if nah { + nah = false + return + } + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) + }) + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + // we got the change + c.Assert(chg, check.NotNil) + c.Assert(err, check.IsNil) + + // but only after recovering + c.Check(meter.Labels, testutil.Contains, "Waiting for server to restart") +} + +func (s *SnapOpSuite) TestInstall(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "channel": "candidate", + }) + s.srv.channel = "candidate" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "candidate", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallFromTrack(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "channel": "3.4/stable", + }) + s.srv.channel = "3.4/stable" + } + + s.RedirectClientToTestServer(s.srv.handle) + // snap install --channel=3.4 means 3.4/stable, this is what we test here + rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "3.4", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/stable\) 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallFromBranch(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "channel": "3.4/hotfix-1", + }) + s.srv.channel = "3.4/hotfix-1" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "3.4/hotfix-1", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/hotfix-1\) 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallDevMode(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "devmode": true, + "channel": "beta", + }) + s.srv.channel = "beta" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--channel", "beta", "--devmode", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallClassic(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "classic": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--classic", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallUnaliased(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "unaliased": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--unaliased", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func testForm(r *http.Request, c *check.C) *multipart.Form { + contentType := r.Header.Get("Content-Type") + mediaType, params, err := mime.ParseMediaType(contentType) + c.Assert(err, check.IsNil) + c.Assert(params["boundary"], check.Matches, ".{10,}") + c.Check(mediaType, check.Equals, "multipart/form-data") + + form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(1 << 20) + c.Assert(err, check.IsNil) + + return form +} + +func formFile(form *multipart.Form, c *check.C) (name, filename string, content []byte) { + c.Assert(form.File, check.HasLen, 1) + + for name, fheaders := range form.File { + c.Assert(fheaders, check.HasLen, 1) + body, err := fheaders[0].Open() + c.Assert(err, check.IsNil) + defer body.Close() + filename = fheaders[0].Filename + content, err = ioutil.ReadAll(body) + c.Assert(err, check.IsNil) + + return name, filename, content + } + + return "", "", nil +} + +func (s *SnapOpSuite) TestInstallPath(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["devmode"], check.IsNil) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 2) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser().ParseArgs([]string{"install", snapPath}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallPathDevMode(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["devmode"], check.DeepEquals, []string{"true"}) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 3) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser().ParseArgs([]string{"install", "--devmode", snapPath}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallPathClassic(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["classic"], check.DeepEquals, []string{"true"}) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 3) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser().ParseArgs([]string{"install", "--classic", snapPath}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestInstallPathDangerous(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + form := testForm(r, c) + defer form.RemoveAll() + + c.Check(form.Value["action"], check.DeepEquals, []string{"install"}) + c.Check(form.Value["dangerous"], check.DeepEquals, []string{"true"}) + c.Check(form.Value["snap-path"], check.NotNil) + c.Check(form.Value, check.HasLen, 3) + + name, _, body := formFile(form, c) + c.Check(name, check.Equals, "snap") + c.Check(string(body), check.Equals, "snap-data") + } + + snapBody := []byte("snap-data") + s.RedirectClientToTestServer(s.srv.handle) + snapPath := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snapPath, snapBody, 0644) + c.Assert(err, check.IsNil) + + rest, err := snap.Parser().ParseArgs([]string{"install", "--dangerous", snapPath}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestRevertRunthrough(c *check.C) { + s.srv.total = 4 + s.srv.channel = "potato" + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "revert", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"revert", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + // tracking channel is "" in the test server + c.Check(s.Stdout(), check.Equals, `foo reverted to 1.0 +This leaves foo tracking . +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) runRevertTest(c *check.C, opts *client.SnapOptions) { + modes := []struct { + enabled bool + name string + }{ + {opts.DevMode, "devmode"}, + {opts.JailMode, "jailmode"}, + {opts.Classic, "classic"}, + } + + s.srv.checker = func(r *http.Request) { + + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + d := DecodedRequestBody(c, r) + + n := 1 + c.Check(d["action"], check.Equals, "revert") + + for _, mode := range modes { + if mode.enabled { + n++ + c.Check(d[mode.name], check.Equals, true) + } else { + c.Check(d[mode.name], check.IsNil) + } + } + c.Check(d, check.HasLen, n) + } + + s.RedirectClientToTestServer(s.srv.handle) + + cmd := []string{"revert", "foo"} + for _, mode := range modes { + if mode.enabled { + cmd = append(cmd, "--"+mode.name) + } + } + + rest, err := snap.Parser().ParseArgs(cmd) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "foo reverted to 1.0\n") + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestRevertNoMode(c *check.C) { + s.runRevertTest(c, &client.SnapOptions{}) +} + +func (s *SnapOpSuite) TestRevertDevMode(c *check.C) { + s.runRevertTest(c, &client.SnapOptions{DevMode: true}) +} + +func (s *SnapOpSuite) TestRevertJailMode(c *check.C) { + s.runRevertTest(c, &client.SnapOptions{JailMode: true}) +} + +func (s *SnapOpSuite) TestRevertClassic(c *check.C) { + s.runRevertTest(c, &client.SnapOptions{Classic: true}) +} + +func (s *SnapOpSuite) TestRevertMissingName(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"revert"}) + c.Assert(err, check.NotNil) + c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") +} + +func (s *SnapSuite) TestRefreshList(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + c.Check(r.URL.Query().Get("select"), check.Equals, "refresh") + fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2update1", "developer": "bar", "revision":17,"summary":"some summary"}]}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"refresh", "--list"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Developer +Notes +foo +4.2update1 +17 +bar +-.* +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestRefreshTime(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/system-info") + fmt.Fprintln(w, `{"type": "sync", "status-code": 200, "result": {"refresh": {"schedule": "00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59", "last": "2017-04-25T17:35:00+0200", "next": "2017-04-26T00:58:00+0200"}}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser().ParseArgs([]string{"refresh", "--time"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `schedule: 00:00-04:59/5:00-10:59/11:00-16:59/17:00-23:59 +last: 2017-04-25T17:35:00+0200 +next: 2017-04-26T00:58:00+0200 +`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestRefreshListErr(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--list", "--beta"}) + c.Check(err, check.ErrorMatches, "--list does not take .* flags") +} + +func (s *SnapOpSuite) TestRefreshOne(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from 'bar' refreshed`) + +} + +func (s *SnapOpSuite) TestRefreshOneSwitchChannel(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "channel": "beta", + }) + s.srv.channel = "beta" + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from 'bar' refreshed`) +} + +func (s *SnapOpSuite) TestRefreshOneClassic(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "classic": true, + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--classic", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneDevmode(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "devmode": true, + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--devmode", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneJailmode(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "jailmode": true, + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--jailmode", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneIgnoreValidation(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "ignore-validation": true, + }) + } + _, err := snap.Parser().ParseArgs([]string{"refresh", "--ignore-validation", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestRefreshOneModeErr(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--jailmode", "--devmode", "one"}) + c.Assert(err, check.ErrorMatches, `cannot use devmode and jailmode flags together`) +} + +func (s *SnapOpSuite) TestRefreshOneChanErr(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "--channel=foo", "one"}) + c.Assert(err, check.ErrorMatches, `Please specify a single channel`) +} + +func (s *SnapOpSuite) TestRefreshAllChannel(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) TestRefreshManyChannel(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--beta", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) TestRefreshManyIgnoreValidation(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--ignore-validation", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name must be specified when ignoring validation`) +} + +func (s *SnapOpSuite) TestRefreshAllModeFlags(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"refresh", "--devmode"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) runTryTest(c *check.C, opts *client.SnapOptions) { + // pass relative path to cmd + tryDir := "some-dir" + + modes := []struct { + enabled bool + name string + }{ + {opts.DevMode, "devmode"}, + {opts.JailMode, "jailmode"}, + {opts.Classic, "classic"}, + } + + s.srv.checker = func(r *http.Request) { + // ensure the client always sends the absolute path + fullTryDir, err := filepath.Abs(tryDir) + c.Assert(err, check.IsNil) + + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + form := testForm(r, c) + defer form.RemoveAll() + + c.Assert(form.Value["action"], check.HasLen, 1) + c.Assert(form.Value["snap-path"], check.HasLen, 1) + c.Check(form.File, check.HasLen, 0) + c.Check(form.Value["action"][0], check.Equals, "try") + c.Check(form.Value["snap-path"][0], check.Matches, regexp.QuoteMeta(fullTryDir)) + + for _, mode := range modes { + if mode.enabled { + c.Assert(form.Value[mode.name], check.HasLen, 1) + c.Check(form.Value[mode.name][0], check.Equals, "true") + } else { + c.Check(form.Value[mode.name], check.IsNil) + } + } + } + + s.RedirectClientToTestServer(s.srv.handle) + + cmd := []string{"try", tryDir} + for _, mode := range modes { + if mode.enabled { + cmd = append(cmd, "--"+mode.name) + } + } + + rest, err := snap.Parser().ParseArgs(cmd) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, fmt.Sprintf(`(?sm).*foo 1.0 mounted from .*%s`, tryDir)) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestTryNoMode(c *check.C) { + s.runTryTest(c, &client.SnapOptions{}) +} + +func (s *SnapOpSuite) TestTryDevMode(c *check.C) { + s.runTryTest(c, &client.SnapOptions{DevMode: true}) +} + +func (s *SnapOpSuite) TestTryJailMode(c *check.C) { + s.runTryTest(c, &client.SnapOptions{JailMode: true}) +} + +func (s *SnapOpSuite) TestTryClassic(c *check.C) { + s.runTryTest(c, &client.SnapOptions{Classic: true}) +} + +func (s *SnapOpSuite) TestTryNoSnapDirErrors(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(202) + fmt.Fprintln(w, ` +{ + "type": "error", + "result": { + "message":"error from server", + "kind":"snap-not-a-snap" + }, + "status-code": 400 +} +`) + + }) + + cmd := []string{"try", "/"} + _, err := snap.Parser().ParseArgs(cmd) + c.Assert(err, check.ErrorMatches, `"/" does not contain an unpacked snap. + +Try "snapcraft prime" in your project directory, then "snap try" again.`) +} + +func (s *SnapSuite) TestInstallChannelDuplicationError(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"install", "--edge", "--beta", "some-snap"}) + c.Assert(err, check.ErrorMatches, "Please specify a single channel") +} + +func (s *SnapSuite) TestRefreshChannelDuplicationError(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"refresh", "--edge", "--beta", "some-snap"}) + c.Assert(err, check.ErrorMatches, "Please specify a single channel") +} + +func (s *SnapOpSuite) TestInstallFromChannel(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "channel": "edge", + }) + s.srv.channel = "edge" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"install", "--edge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(edge\) 1.0 from 'bar' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestEnable(c *check.C) { + s.srv.total = 3 + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "enable", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"enable", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo enabled`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestDisable(c *check.C) { + s.srv.total = 3 + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "disable", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"disable", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo disabled`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestRemove(c *check.C) { + s.srv.total = 3 + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "remove", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"remove", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo removed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestRemoveManyRevision(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"remove", "--revision=17", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify the revision`) +} + +func (s *SnapOpSuite) TestRemoveMany(c *check.C) { + total := 3 + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "remove", + "snaps": []interface{}{"one", "two"}, + }) + + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + case 2: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["one","two"]}}}`) + default: + c.Fatalf("expected to get %d requests, now on %d", total, n+1) + } + + n++ + }) + + rest, err := snap.Parser().ParseArgs([]string{"remove", "one", "two"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*one removed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*two removed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, total) +} + +func (s *SnapOpSuite) TestInstallManyChannel(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"install", "--beta", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name is needed to specify mode or channel flags`) +} + +func (s *SnapOpSuite) TestInstallManyMixFileAndStore(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser().ParseArgs([]string{"install", "store-snap", "./local.snap"}) + c.Assert(err, check.ErrorMatches, `only one snap file can be installed at a time`) +} + +func (s *SnapOpSuite) TestInstallMany(c *check.C) { + total := 4 + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "snaps": []interface{}{"one", "two"}, + }) + + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) + case 2: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["one","two"]}}}`) + case 3: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps") + fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "one", "status": "active", "version": "1.0", "developer": "bar", "revision":42, "channel":"stable"},{"name": "two", "status": "active", "version": "2.0", "developer": "baz", "revision":42, "channel":"edge"}]}\n`) + + default: + c.Fatalf("expected to get %d requests, now on %d", total, n+1) + } + + n++ + }) + + rest, err := snap.Parser().ParseArgs([]string{"install", "one", "two"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + // note that (stable) is omitted + c.Check(s.Stdout(), check.Matches, `(?sm).*one 1.0 from 'bar' installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*two \(edge\) 2.0 from 'baz' installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, total) +} + +func (s *SnapOpSuite) TestNoWait(c *check.C) { + s.srv.checker = func(r *http.Request) {} + + cmds := [][]string{ + {"remove", "--no-wait", "foo"}, + {"remove", "--no-wait", "foo", "bar"}, + {"install", "--no-wait", "foo"}, + {"install", "--no-wait", "foo", "bar"}, + {"revert", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo", "bar"}, + {"enable", "--no-wait", "foo"}, + {"disable", "--no-wait", "foo"}, + {"try", "--no-wait", "."}, + } + + s.RedirectClientToTestServer(s.srv.handle) + for _, cmd := range cmds { + rest, err := snap.Parser().ParseArgs(cmd) + c.Assert(err, check.IsNil, check.Commentf("%v", cmd)) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, "(?sm)42\n") + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.srv.n, check.Equals, 1) + // reset + s.srv.n = 0 + s.stdout.Reset() + } +} + +func (s *SnapOpSuite) TestSwitchHappy(c *check.C) { + s.srv.total = 3 + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "switch", + "channel": "beta", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs([]string{"switch", "--beta", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "beta" channel`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + +func (s *SnapOpSuite) TestSwitchUnhappy(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"switch"}) + c.Assert(err, check.ErrorMatches, "the required argument `` was not provided") +} + +func (s *SnapOpSuite) TestSwitchAlsoUnhappy(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"switch", "foo"}) + c.Assert(err, check.ErrorMatches, `missing --channel= parameter`) +} diff --git a/cmd/snap/cmd_unalias.go b/cmd/snap/cmd_unalias.go new file mode 100644 index 00000000..1f33b85f --- /dev/null +++ b/cmd/snap/cmd_unalias.go @@ -0,0 +1,68 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +type cmdUnalias struct { + Positionals struct { + AliasOrSnap aliasOrSnap `required:"yes"` + } `positional-args:"true"` +} + +var shortUnaliasHelp = i18n.G("Unalias a manual alias or an entire snap") +var longUnaliasHelp = i18n.G(` +The unalias command tears down a manual alias when given one or disables all aliases of a snap, removing also all manual ones, when given a snap name. +`) + +func init() { + addCommand("unalias", shortUnaliasHelp, longUnaliasHelp, func() flags.Commander { + return &cmdUnalias{} + }, nil, []argDesc{ + // TRANSLATORS: This needs to be wrapped in <>s. + {name: i18n.G("")}, + }) +} + +func (x *cmdUnalias) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + id, err := cli.Unalias(string(x.Positionals.AliasOrSnap)) + if err != nil { + return err + } + + chg, err := wait(cli, id) + if err != nil { + return err + } + if err := showAliasChanges(chg); err != nil { + return err + } + + return nil +} diff --git a/cmd/snap/cmd_unalias_test.go b/cmd/snap/cmd_unalias_test.go new file mode 100644 index 00000000..886aaa6b --- /dev/null +++ b/cmd/snap/cmd_unalias_test.go @@ -0,0 +1,75 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestUnaliasHelp(c *C) { + msg := `Usage: + snap.test [OPTIONS] unalias [] + +The unalias command tears down a manual alias when given one or disables all +aliases of a snap, removing also all manual ones, when given a snap name. + +Application Options: + --version Print the version and exit + +Help Options: + -h, --help Show this help message +` + rest, err := Parser().ParseArgs([]string{"unalias", "--help"}) + c.Assert(err.Error(), Equals, msg) + c.Assert(rest, DeepEquals, []string{}) +} + +func (s *SnapSuite) TestUnalias(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/aliases": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "unalias", + "snap": "alias1", + "alias": "alias1", + }) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done", "data": {"aliases-removed": [{"alias": "alias1", "snap": "foo", "app": "foo"}]}}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser().ParseArgs([]string{"unalias", "alias1"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, ""+ + "Removed:\n"+ + " - foo as alias1\n", + ) + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_userd.go b/cmd/snap/cmd_userd.go new file mode 100644 index 00000000..17b75c5a --- /dev/null +++ b/cmd/snap/cmd_userd.go @@ -0,0 +1,74 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/userd" +) + +type cmdUserd struct { + userd userd.Userd +} + +var shortUserdHelp = i18n.G("Start the userd service") +var longUserdHelp = i18n.G("The userd command starts the snap user session service.") + +func init() { + cmd := addCommand("userd", + shortAbortHelp, + longAbortHelp, + func() flags.Commander { + return &cmdUserd{} + }, + nil, + []argDesc{}, + ) + cmd.hidden = true +} + +func (x *cmdUserd) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if err := x.userd.Init(); err != nil { + return err + } + x.userd.Start() + + ch := make(chan os.Signal) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) + select { + case sig := <-ch: + fmt.Fprintf(Stdout, "Exiting on %s.\n", sig) + case <-x.userd.Dying(): + // something called Stop() + } + + return x.userd.Stop() +} diff --git a/cmd/snap/cmd_userd_test.go b/cmd/snap/cmd_userd_test.go new file mode 100644 index 00000000..b9385bd1 --- /dev/null +++ b/cmd/snap/cmd_userd_test.go @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "os" + "syscall" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/testutil" +) + +type userdSuite struct { + BaseSnapSuite + testutil.DBusTest + + restoreLogger func() +} + +var _ = Suite(&userdSuite{}) + +func (s *userdSuite) SetUpTest(c *C) { + s.BaseSnapSuite.SetUpTest(c) + s.DBusTest.SetUpTest(c) + + _, s.restoreLogger = logger.MockLogger() +} + +func (s *userdSuite) TearDownTest(c *C) { + s.BaseSnapSuite.TearDownTest(c) + s.DBusTest.TearDownTest(c) + + s.restoreLogger() +} + +func (s *userdSuite) TestUserdBadCommandline(c *C) { + _, err := snap.Parser().ParseArgs([]string{"userd", "extra-arg"}) + c.Assert(err, ErrorMatches, "too many arguments for command") +} + +func (s *userdSuite) TestUserd(c *C) { + go func() { + defer func() { + me, err := os.FindProcess(os.Getpid()) + c.Assert(err, IsNil) + me.Signal(syscall.SIGUSR1) + }() + + needle := "io.snapcraft.Launcher" + for i := 0; i < 10; i++ { + for _, objName := range s.SessionBus.Names() { + if objName == needle { + return + } + time.Sleep(1 * time.Second) + } + + } + c.Fatalf("%s does not appeared on the bus", needle) + }() + + rest, err := snap.Parser().ParseArgs([]string{"userd"}) + c.Assert(err, IsNil) + c.Check(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, "Exiting on user defined signal 1.\n") +} diff --git a/cmd/snap/cmd_version.go b/cmd/snap/cmd_version.go new file mode 100644 index 00000000..1be76658 --- /dev/null +++ b/cmd/snap/cmd_version.go @@ -0,0 +1,78 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortVersionHelp = i18n.G("Shows version details") +var longVersionHelp = i18n.G(` +The version command displays the versions of the running client, server, +and operating system. +`) + +type cmdVersion struct{} + +func init() { + addCommand("version", shortVersionHelp, longVersionHelp, func() flags.Commander { return &cmdVersion{} }, nil, nil) +} + +func (cmd cmdVersion) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + printVersions() + return nil +} + +func printVersions() error { + sv, err := Client().ServerVersion() + if err != nil { + sv = &client.ServerVersion{ + Version: i18n.G("unavailable"), + Series: "-", + OSID: "-", + OSVersionID: "-", + } + } + + w := tabWriter() + + fmt.Fprintf(w, "snap\t%s\n", cmd.Version) + fmt.Fprintf(w, "snapd\t%s\n", sv.Version) + fmt.Fprintf(w, "series\t%s\n", sv.Series) + if sv.OnClassic { + fmt.Fprintf(w, "%s\t%s\n", sv.OSID, sv.OSVersionID) + } + if sv.KernelVersion != "" { + fmt.Fprintf(w, "kernel\t%s\n", sv.KernelVersion) + } + w.Flush() + + return err +} diff --git a/cmd/snap/cmd_version_test.go b/cmd/snap/cmd_version_test.go new file mode 100644 index 00000000..59cdee2d --- /dev/null +++ b/cmd/snap/cmd_version_test.go @@ -0,0 +1,59 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestVersionCommandOnClassic(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + _, err := snap.Parser().ParseArgs([]string{"version"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\nubuntu 12.34\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestVersionCommandOnAllSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "--version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + _, err := snap.Parser().ParseArgs([]string{"version"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\n") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_watch.go b/cmd/snap/cmd_watch.go new file mode 100644 index 00000000..a92f4c42 --- /dev/null +++ b/cmd/snap/cmd_watch.go @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdWatch struct{ changeIDMixin } + +var shortWatchHelp = i18n.G("Watch a change in progress") +var longWatchHelp = i18n.G(` +The watch command waits for the given change-id to finish and shows progress +(if available). +`) + +func init() { + addCommand("watch", shortWatchHelp, longWatchHelp, func() flags.Commander { + return &cmdWatch{} + }, changeIDMixinOptDesc, changeIDMixinArgDesc) +} + +func (x *cmdWatch) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + cli := Client() + id, err := x.GetChangeID(cli) + if err != nil { + return err + } + _, err = wait(cli, id) + + return err +} diff --git a/cmd/snap/cmd_watch_test.go b/cmd/snap/cmd_watch_test.go new file mode 100644 index 00000000..4dd54614 --- /dev/null +++ b/cmd/snap/cmd_watch_test.go @@ -0,0 +1,73 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/progress/progresstest" +) + +var fmtWatchChangeJSON = `{"type": "sync", "result": { + "id": "42", + "kind": "some-kind", + "summary": "some summary...", + "status": "Doing", + "ready": false, + "tasks": [{"id": "84", "kind": "bar", "summary": "some summary", "status": "Doing", "progress": {"label": "my-snap", "done": %d, "total": %d}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] +}}` + +func (s *SnapSuite) TestCmdWatch(c *C) { + meter := &progresstest.Meter{} + defer progress.MockMeter(meter)() + restore := snap.MockMaxGoneTime(time.Millisecond) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/42") + fmt.Fprintf(w, fmtWatchChangeJSON, 0, 100*1024) + case 1: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/42") + fmt.Fprintf(w, fmtWatchChangeJSON, 50*1024, 100*1024) + case 2: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/changes/42") + fmt.Fprintln(w, `{"type": "sync", "result": {"id": "42", "ready": true, "status": "Done"}}`) + } + n++ + }) + + _, err := snap.Parser().ParseArgs([]string{"watch", "42"}) + c.Assert(err, IsNil) + c.Check(n, Equals, 3) + + c.Check(meter.Values, DeepEquals, []float64{51200}) +} diff --git a/cmd/snap/cmd_whoami.go b/cmd/snap/cmd_whoami.go new file mode 100644 index 00000000..29231e60 --- /dev/null +++ b/cmd/snap/cmd_whoami.go @@ -0,0 +1,56 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortWhoAmIHelp = i18n.G("Prints the email the user is logged in with.") +var longWhoAmIHelp = i18n.G(` +The whoami command prints the email the user is logged in with. +`) + +type cmdWhoAmI struct{} + +func init() { + addCommand("whoami", shortWhoAmIHelp, longWhoAmIHelp, func() flags.Commander { return &cmdWhoAmI{} }, nil, nil) +} + +func (cmd cmdWhoAmI) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + email, err := Client().WhoAmI() + if err != nil { + return err + } + if email == "" { + // just printing nothing looks weird (as if something had gone wrong) + email = "-" + } + fmt.Fprintln(Stdout, i18n.G("email:"), email) + return nil +} diff --git a/cmd/snap/complete.go b/cmd/snap/complete.go new file mode 100644 index 00000000..23ac2116 --- /dev/null +++ b/cmd/snap/complete.go @@ -0,0 +1,457 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" +) + +type installedSnapName string + +func (s installedSnapName) Complete(match string) []flags.Completion { + snaps, err := Client().List(nil, nil) + if err != nil { + return nil + } + + ret := make([]flags.Completion, 0, len(snaps)) + for _, snap := range snaps { + if strings.HasPrefix(snap.Name, match) { + ret = append(ret, flags.Completion{Item: snap.Name}) + } + } + + return ret +} + +func completeFromSortedFile(filename, match string) ([]flags.Completion, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var ret []flags.Completion + + // TODO: look into implementing binary search + // e.g. https://github.com/pts/pts-line-bisect/ + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if line < match { + continue + } + if !strings.HasPrefix(line, match) { + break + } + ret = append(ret, flags.Completion{Item: line}) + if len(ret) > 10000 { + // too many matches; slow machines could take too long to process this + // e.g. the bbb takes ~1s to process ~2M entries (i.e. to reach the + // point of asking the user if they actually want to see that many + // results). 10k ought to be enough for anybody. + break + } + } + + return ret, nil +} + +type remoteSnapName string + +func (s remoteSnapName) Complete(match string) []flags.Completion { + if ret, err := completeFromSortedFile(dirs.SnapNamesFile, match); err == nil { + return ret + } + + if len(match) < 3 { + return nil + } + snaps, _, err := Client().Find(&client.FindOptions{ + Prefix: true, + Query: match, + }) + if err != nil { + return nil + } + ret := make([]flags.Completion, len(snaps)) + for i, snap := range snaps { + ret[i] = flags.Completion{Item: snap.Name} + } + return ret +} + +type anySnapName string + +func (s anySnapName) Complete(match string) []flags.Completion { + res := installedSnapName(s).Complete(match) + seen := make(map[string]bool) + for _, x := range res { + seen[x.Item] = true + } + + for _, x := range remoteSnapName(s).Complete(match) { + if !seen[x.Item] { + res = append(res, x) + } + } + + return res +} + +type changeID string + +func (s changeID) Complete(match string) []flags.Completion { + changes, err := Client().Changes(&client.ChangesOptions{Selector: client.ChangesAll}) + if err != nil { + return nil + } + + ret := make([]flags.Completion, 0, len(changes)) + for _, change := range changes { + if strings.HasPrefix(change.ID, match) { + ret = append(ret, flags.Completion{Item: change.ID}) + } + } + + return ret +} + +type assertTypeName string + +func (n assertTypeName) Complete(match string) []flags.Completion { + cli := Client() + names, err := cli.AssertionTypes() + if err != nil { + return nil + } + ret := make([]flags.Completion, 0, len(names)) + for _, name := range names { + if strings.HasPrefix(name, match) { + ret = append(ret, flags.Completion{Item: name}) + } + } + + return ret +} + +type keyName string + +func (s keyName) Complete(match string) []flags.Completion { + var res []flags.Completion + asserts.NewGPGKeypairManager().Walk(func(_ asserts.PrivateKey, _ string, uid string) error { + if strings.HasPrefix(uid, match) { + res = append(res, flags.Completion{Item: uid}) + } + return nil + }) + return res +} + +type disconnectSlotOrPlugSpec struct { + SnapAndName +} + +func (dps disconnectSlotOrPlugSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: dps.SnapAndName, + slots: true, + plugs: true, + connected: true, + disconnected: false, + } + return spec.Complete(match) +} + +type disconnectSlotSpec struct { + SnapAndName +} + +// TODO: look at what the previous arg is, and filter accordingly +func (dss disconnectSlotSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: dss.SnapAndName, + slots: true, + plugs: false, + connected: true, + disconnected: false, + } + return spec.Complete(match) +} + +type connectPlugSpec struct { + SnapAndName +} + +func (cps connectPlugSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: cps.SnapAndName, + slots: false, + plugs: true, + connected: false, + disconnected: true, + } + return spec.Complete(match) +} + +type connectSlotSpec struct { + SnapAndName +} + +// TODO: look at what the previous arg is, and filter accordingly +func (css connectSlotSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: css.SnapAndName, + slots: true, + plugs: false, + connected: false, + disconnected: true, + } + return spec.Complete(match) +} + +type interfacesSlotOrPlugSpec struct { + SnapAndName +} + +func (is interfacesSlotOrPlugSpec) Complete(match string) []flags.Completion { + spec := &interfaceSpec{ + SnapAndName: is.SnapAndName, + slots: true, + plugs: true, + connected: true, + disconnected: true, + } + return spec.Complete(match) +} + +type interfaceSpec struct { + SnapAndName + slots bool + plugs bool + connected bool + disconnected bool +} + +func (spec *interfaceSpec) connFilter(numConns int) bool { + if spec.connected && numConns > 0 { + return true + } + if spec.disconnected && numConns == 0 { + return true + } + + return false +} + +func (spec *interfaceSpec) Complete(match string) []flags.Completion { + // Parse what the user typed so far, it can be either + // nothing (""), a "snap", a "snap:" or a "snap:name". + parts := strings.SplitN(match, ":", 2) + + // Ask snapd about available interfaces. + ifaces, err := Client().Connections() + if err != nil { + return nil + } + + snaps := make(map[string]bool) + + var ret []flags.Completion + + var prefix string + if len(parts) == 2 { + // The user typed the colon, means they know the snap they want; + // go with that. + prefix = parts[1] + snaps[parts[0]] = true + } else { + // The user is about to or has started typing a snap name but didn't + // reach the colon yet. Offer plugs for snaps with names that start + // like that. + snapPrefix := parts[0] + if spec.plugs { + for _, plug := range ifaces.Plugs { + if strings.HasPrefix(plug.Snap, snapPrefix) && spec.connFilter(len(plug.Connections)) { + snaps[plug.Snap] = true + } + } + } + if spec.slots { + for _, slot := range ifaces.Slots { + if strings.HasPrefix(slot.Snap, snapPrefix) && spec.connFilter(len(slot.Connections)) { + snaps[slot.Snap] = true + } + } + } + } + + if len(snaps) == 1 { + for snapName := range snaps { + actualName := snapName + if spec.plugs { + if spec.connected && snapName == "" { + actualName = "core" + } + for _, plug := range ifaces.Plugs { + if plug.Snap == actualName && strings.HasPrefix(plug.Name, prefix) && spec.connFilter(len(plug.Connections)) { + // TODO: in the future annotate plugs that can take + // multiple connection sensibly and don't skip those even + // if they have connections already. + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:%s", snapName, plug.Name), Description: "plug"}) + } + } + } + if spec.slots { + if actualName == "" { + actualName = "core" + } + for _, slot := range ifaces.Slots { + if slot.Snap == actualName && strings.HasPrefix(slot.Name, prefix) && spec.connFilter(len(slot.Connections)) { + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:%s", snapName, slot.Name), Description: "slot"}) + } + } + } + } + } else { + snaps: + for snapName := range snaps { + if spec.plugs { + for _, plug := range ifaces.Plugs { + if plug.Snap == snapName && spec.connFilter(len(plug.Connections)) { + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:", snapName)}) + continue snaps + } + } + } + if spec.slots { + for _, slot := range ifaces.Slots { + if slot.Snap == snapName && spec.connFilter(len(slot.Connections)) { + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:", snapName)}) + continue snaps + } + } + } + } + } + + return ret +} + +type interfaceName string + +func (s interfaceName) Complete(match string) []flags.Completion { + ifaces, err := Client().Interfaces(nil) + if err != nil { + return nil + } + + ret := make([]flags.Completion, 0, len(ifaces)) + for _, iface := range ifaces { + if strings.HasPrefix(iface.Name, match) { + ret = append(ret, flags.Completion{Item: iface.Name, Description: iface.Summary}) + } + } + + return ret +} + +type appName string + +func (s appName) Complete(match string) []flags.Completion { + cli := Client() + apps, err := cli.Apps(nil, client.AppOptions{}) + if err != nil { + return nil + } + + var ret []flags.Completion + for _, app := range apps { + if app.IsService() { + continue + } + name := snap.JoinSnapApp(app.Snap, app.Name) + if !strings.HasPrefix(name, match) { + continue + } + ret = append(ret, flags.Completion{Item: name}) + } + + return ret +} + +type serviceName string + +func (s serviceName) Complete(match string) []flags.Completion { + cli := Client() + apps, err := cli.Apps(nil, client.AppOptions{Service: true}) + if err != nil { + return nil + } + + snaps := map[string]bool{} + var ret []flags.Completion + for _, app := range apps { + if !app.IsService() { + continue + } + if !snaps[app.Snap] { + snaps[app.Snap] = true + ret = append(ret, flags.Completion{Item: app.Snap}) + } + ret = append(ret, flags.Completion{Item: app.Snap + "." + app.Name}) + } + + return ret +} + +type aliasOrSnap string + +func (s aliasOrSnap) Complete(match string) []flags.Completion { + aliases, err := Client().Aliases() + if err != nil { + return nil + } + var ret []flags.Completion + for snap, aliases := range aliases { + if strings.HasPrefix(snap, match) { + ret = append(ret, flags.Completion{Item: snap}) + } + for alias, status := range aliases { + if status.Status == "disabled" { + continue + } + if strings.HasPrefix(alias, match) { + ret = append(ret, flags.Completion{Item: alias}) + } + } + } + return ret +} diff --git a/cmd/snap/error.go b/cmd/snap/error.go new file mode 100644 index 00000000..364ec3d6 --- /dev/null +++ b/cmd/snap/error.go @@ -0,0 +1,176 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "bytes" + "errors" + "fmt" + "go/doc" + "os" + "os/user" + "strings" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" +) + +var errorPrefix = i18n.G("error: %v\n") + +func termSize() (width, height int) { + if f, ok := Stdout.(*os.File); ok { + width, height, _ = terminal.GetSize(int(f.Fd())) + } + + if width <= 0 { + width = int(osutil.GetenvInt64("COLUMNS")) + } + + if height <= 0 { + height = int(osutil.GetenvInt64("LINES")) + } + + if width < 40 { + width = 80 + } + + if height < 15 { + height = 25 + } + + return width, height +} + +func fill(para string, indent int) string { + width, _ := termSize() + + if width > 100 { + width = 100 + } + + // some terminals aren't happy about writing in the last + // column (they'll add line for you). We could check terminfo + // for "sam" (semi_auto_right_margin), but that's a lot of + // work just for this. + width-- + + var buf bytes.Buffer + doc.ToText(&buf, para, strings.Repeat(" ", indent), "", width-indent) + + return strings.TrimSpace(buf.String()) +} + +func errorToCmdMessage(snapName string, e error, opts *client.SnapOptions) (string, error) { + // do this here instead of in the caller for more DRY + err, ok := e.(*client.Error) + if !ok { + return "", e + } + + // FIXME: using err.Message in user-facing messaging is not + // l10n-friendly, and probably means we're missing ad-hoc messaging. + + isError := true + usesSnapName := true + var msg string + switch err.Kind { + case client.ErrorKindSnapNotFound: + // FIXME: the snap store _is_ sending us a different message when a + // snap does not exist vs. when it does not exist for the current + // arch/channel/revision. Surface that here somehow! + + msg = i18n.G("snap %q not found") + if snapName == "" { + errValStr, ok := err.Value.(string) + if ok && errValStr != "" { + snapName = errValStr + } + } + if opts != nil { + if opts.Revision != "" { + // TRANSLATORS: %%q will become a %q for the snap name; %q is whatever foo the user used for --revision=foo + msg = fmt.Sprintf(i18n.G("snap %%q not found (at least at revision %q)"), opts.Revision) + } else if opts.Channel != "" { + // (note --revision overrides --channel) + + // TRANSLATORS: %%q will become a %q for the snap name; %q is whatever foo the user used for --channel=foo + msg = fmt.Sprintf(i18n.G("snap %%q not found (at least in channel %q)"), opts.Channel) + } + } + case client.ErrorKindSnapAlreadyInstalled: + isError = false + msg = i18n.G(`snap %q is already installed, see "snap refresh --help"`) + case client.ErrorKindSnapNeedsDevMode: + msg = i18n.G(` +The publisher of snap %q has indicated that they do not consider this revision +to be of production quality and that it is only meant for development or testing +at this point. As a consequence this snap will not refresh automatically and may +perform arbitrary system changes outside of the security sandbox snaps are +generally confined to, which may put your system at risk. + +If you understand and want to proceed repeat the command including --devmode; +if instead you want to install the snap forcing it into strict confinement +repeat the command including --jailmode.`) + case client.ErrorKindSnapNeedsClassic: + msg = i18n.G(` +This revision of snap %q was published using classic confinement and thus may +perform arbitrary system changes outside of the security sandbox that snaps are +usually confined to, which may put your system at risk. + +If you understand and want to proceed repeat the command including --classic. +`) + case client.ErrorKindLoginRequired: + usesSnapName = false + u, _ := user.Current() + if u != nil && u.Username == "root" { + // TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”) + msg = fmt.Sprintf(i18n.G(`%s (see "snap login --help")`), err.Message) + } else { + // TRANSLATORS: %s is an error message (e.g. “cannot yadda yadda: permission denied”) + msg = fmt.Sprintf(i18n.G(`%s (try with sudo)`), err.Message) + } + case client.ErrorKindSnapLocal: + msg = i18n.G("snap %q is local") + case client.ErrorKindNoUpdateAvailable: + isError = false + msg = i18n.G("snap %q has no updates available") + case client.ErrorKindSnapNotInstalled: + isError = false + usesSnapName = false + msg = err.Message + default: + usesSnapName = false + msg = err.Message + } + + if usesSnapName { + msg = fmt.Sprintf(msg, snapName) + } + // 3 is the %v\n, which will be present in any locale + msg = fill(msg, len(errorPrefix)-3) + if isError { + return "", errors.New(msg) + } + + return msg, nil +} diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go new file mode 100644 index 00000000..47ca1c30 --- /dev/null +++ b/cmd/snap/export_test.go @@ -0,0 +1,140 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "os/user" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/store" +) + +var RunMain = run + +var ( + CreateUserDataDirs = createUserDataDirs + SnapRunApp = snapRunApp + SnapRunHook = snapRunHook + Wait = wait + ResolveApp = resolveApp + IsReexeced = isReexeced + MaybePrintServices = maybePrintServices + MaybePrintCommands = maybePrintCommands + SortByPath = sortByPath +) + +func MockPollTime(d time.Duration) (restore func()) { + d0 := pollTime + pollTime = d + return func() { + pollTime = d0 + } +} + +func MockMaxGoneTime(d time.Duration) (restore func()) { + d0 := maxGoneTime + maxGoneTime = d + return func() { + maxGoneTime = d0 + } +} + +func MockSyscallExec(f func(string, []string, []string) error) (restore func()) { + syscallExecOrig := syscallExec + syscallExec = f + return func() { + syscallExec = syscallExecOrig + } +} + +func MockUserCurrent(f func() (*user.User, error)) (restore func()) { + userCurrentOrig := userCurrent + userCurrent = f + return func() { + userCurrent = userCurrentOrig + } +} + +func MockStoreNew(f func(*store.Config, auth.AuthContext) *store.Store) (restore func()) { + storeNewOrig := storeNew + storeNew = f + return func() { + storeNew = storeNewOrig + } +} + +func MockGetEnv(f func(name string) string) (restore func()) { + osGetenvOrig := osGetenv + osGetenv = f + return func() { + osGetenv = osGetenvOrig + } +} + +func MockMountInfoPath(newMountInfoPath string) (restore func()) { + mountInfoPathOrig := mountInfoPath + mountInfoPath = newMountInfoPath + return func() { + mountInfoPath = mountInfoPathOrig + } +} + +func MockOsReadlink(f func(string) (string, error)) (restore func()) { + osReadlinkOrig := osReadlink + osReadlink = f + return func() { + osReadlink = osReadlinkOrig + } +} + +var AutoImportCandidates = autoImportCandidates + +func AliasInfoLess(snapName1, alias1, cmd1, snapName2, alias2, cmd2 string) bool { + x := aliasInfos{ + &aliasInfo{ + Snap: snapName1, + Alias: alias1, + Command: cmd1, + }, + &aliasInfo{ + Snap: snapName2, + Alias: alias2, + Command: cmd2, + }, + } + return x.Less(0, 1) +} + +func AssertTypeNameCompletion(match string) []flags.Completion { + return assertTypeName("").Complete(match) +} + +func MockIsTerminal(t bool) (restore func()) { + oldIsTerminal := isTerminal + isTerminal = func() bool { return t } + return func() { + isTerminal = oldIsTerminal + } +} + +var Antialias = antialias diff --git a/cmd/snap/gnupg2_test.go b/cmd/snap/gnupg2_test.go new file mode 100644 index 00000000..aa142906 --- /dev/null +++ b/cmd/snap/gnupg2_test.go @@ -0,0 +1,27 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" +) + +// FIXME: drop once gpg2 is the default +var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg2"}) diff --git a/cmd/snap/interfaces_common.go b/cmd/snap/interfaces_common.go new file mode 100644 index 00000000..9f292d74 --- /dev/null +++ b/cmd/snap/interfaces_common.go @@ -0,0 +1,85 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/i18n" +) + +// AttributePair contains a pair of key-value strings +type AttributePair struct { + // The key + Key string + // The value + Value string +} + +// UnmarshalFlag parses a string into an AttributePair +func (ap *AttributePair) UnmarshalFlag(value string) error { + parts := strings.SplitN(value, "=", 2) + if len(parts) < 2 || parts[0] == "" { + ap.Key = "" + ap.Value = "" + return fmt.Errorf(i18n.G("invalid attribute: %q (want key=value)"), value) + } + ap.Key = parts[0] + ap.Value = parts[1] + return nil +} + +// AttributePairSliceToMap converts a slice of AttributePair into a map +func AttributePairSliceToMap(attrs []AttributePair) map[string]string { + result := make(map[string]string) + for _, attr := range attrs { + result[attr.Key] = attr.Value + } + return result +} + +// SnapAndName holds a snap name and a plug or slot name. +type SnapAndName struct { + Snap string + Name string +} + +// UnmarshalFlag unmarshals snap and plug or slot name. +func (sn *SnapAndName) UnmarshalFlag(value string) error { + parts := strings.Split(value, ":") + sn.Snap = "" + sn.Name = "" + switch len(parts) { + case 1: + sn.Snap = parts[0] + case 2: + sn.Snap = parts[0] + sn.Name = parts[1] + // Reject "snap:" (that should be spelled as "snap") + if sn.Name == "" { + sn.Snap = "" + } + } + if sn.Snap == "" && sn.Name == "" { + return fmt.Errorf(i18n.G("invalid value: %q (want snap:name or snap)"), value) + } + return nil +} diff --git a/cmd/snap/interfaces_common_test.go b/cmd/snap/interfaces_common_test.go new file mode 100644 index 00000000..3b46ab9b --- /dev/null +++ b/cmd/snap/interfaces_common_test.go @@ -0,0 +1,106 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + . "github.com/snapcore/snapd/cmd/snap" +) + +type AttributePairSuite struct{} + +var _ = Suite(&AttributePairSuite{}) + +func (s *AttributePairSuite) TestUnmarshalFlagAttributePair(c *C) { + var ap AttributePair + // Typical + err := ap.UnmarshalFlag("key=value") + c.Assert(err, IsNil) + c.Check(ap.Key, Equals, "key") + c.Check(ap.Value, Equals, "value") + // Empty key + err = ap.UnmarshalFlag("=value") + c.Assert(err, ErrorMatches, `invalid attribute: "=value" \(want key=value\)`) + c.Check(ap.Key, Equals, "") + c.Check(ap.Value, Equals, "") + // Empty value + err = ap.UnmarshalFlag("key=") + c.Assert(err, IsNil) + c.Check(ap.Key, Equals, "key") + c.Check(ap.Value, Equals, "") + // Both key and value empty + err = ap.UnmarshalFlag("=") + c.Assert(err, ErrorMatches, `invalid attribute: "=" \(want key=value\)`) + c.Check(ap.Key, Equals, "") + c.Check(ap.Value, Equals, "") + // Value containing = + err = ap.UnmarshalFlag("key=value=more") + c.Assert(err, IsNil) + c.Check(ap.Key, Equals, "key") + c.Check(ap.Value, Equals, "value=more") + // Malformed format + err = ap.UnmarshalFlag("malformed") + c.Assert(err, ErrorMatches, `invalid attribute: "malformed" \(want key=value\)`) + c.Check(ap.Key, Equals, "") + c.Check(ap.Value, Equals, "") +} + +func (s *AttributePairSuite) TestAttributePairSliceToMap(c *C) { + attrs := []AttributePair{ + {"key1", "value1"}, + {"key2", "value2"}, + } + m := AttributePairSliceToMap(attrs) + c.Check(m, DeepEquals, map[string]string{ + "key1": "value1", + "key2": "value2", + }) +} + +type SnapAndNameSuite struct{} + +var _ = Suite(&SnapAndNameSuite{}) + +func (s *SnapAndNameSuite) TestUnmarshalFlag(c *C) { + var sn SnapAndName + // Typical + err := sn.UnmarshalFlag("snap:name") + c.Assert(err, IsNil) + c.Check(sn.Snap, Equals, "snap") + c.Check(sn.Name, Equals, "name") + // Abbreviated + err = sn.UnmarshalFlag("snap") + c.Assert(err, IsNil) + c.Check(sn.Snap, Equals, "snap") + c.Check(sn.Name, Equals, "") + // Invalid + for _, input := range []string{ + "snap:", // Empty name, should be spelled as "snap" + ":", // Both snap and name empty, makes no sense + "snap:name:more", // Name containing :, probably a typo + "", // Empty input + } { + err = sn.UnmarshalFlag(input) + c.Assert(err, ErrorMatches, `invalid value: ".*" \(want snap:name or snap\)`) + c.Check(sn.Snap, Equals, "") + c.Check(sn.Name, Equals, "") + } +} diff --git a/cmd/snap/last.go b/cmd/snap/last.go new file mode 100644 index 00000000..097b8584 --- /dev/null +++ b/cmd/snap/last.go @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type changeIDMixin struct { + LastChangeType string `long:"last"` + Positional struct { + ID changeID `positional-arg-name:""` + } `positional-args:"yes"` +} + +var changeIDMixinOptDesc = map[string]string{ + "last": i18n.G("Select last change of given type (install, refresh, remove, try, auto-refresh etc.)"), +} + +var changeIDMixinArgDesc = []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: i18n.G(""), + // TRANSLATORS: This should probably not start with a lowercase letter. + desc: i18n.G("Change ID"), +}} + +func (l *changeIDMixin) GetChangeID(cli *client.Client) (string, error) { + if l.Positional.ID == "" && l.LastChangeType == "" { + return "", fmt.Errorf(i18n.G("please provide change ID or type with --last=")) + } + + if l.Positional.ID != "" { + if l.LastChangeType != "" { + return "", fmt.Errorf(i18n.G("cannot use change ID and type together")) + } + + return string(l.Positional.ID), nil + } + + kind := l.LastChangeType + // our internal change types use "-snap" postfix but let user skip it and use short form. + if kind == "refresh" || kind == "install" || kind == "remove" || kind == "connect" || kind == "disconnect" || kind == "configure" || kind == "try" { + kind += "-snap" + } + changes, err := cli.Changes(&client.ChangesOptions{Selector: client.ChangesAll}) + if err != nil { + return "", err + } + if len(changes) == 0 { + return "", fmt.Errorf(i18n.G("no changes found")) + } + chg := findLatestChangeByKind(changes, kind) + if chg == nil { + return "", fmt.Errorf(i18n.G("no changes of type %q found"), l.LastChangeType) + } + + return chg.ID, nil +} + +func findLatestChangeByKind(changes []*client.Change, kind string) (latest *client.Change) { + for _, chg := range changes { + if chg.Kind == kind && (latest == nil || latest.SpawnTime.Before(chg.SpawnTime)) { + latest = chg + } + } + return latest +} diff --git a/cmd/snap/main.go b/cmd/snap/main.go new file mode 100644 index 00000000..fbdc8f2e --- /dev/null +++ b/cmd/snap/main.go @@ -0,0 +1,352 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "unicode" + "unicode/utf8" + + "github.com/jessevdk/go-flags" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +func init() { + // set User-Agent for when 'snap' talks to the store directly (snap download etc...) + httputil.SetUserAgentFromVersion(cmd.Version, "snap") + + if osutil.GetenvBool("SNAPD_DEBUG") || osutil.GetenvBool("SNAPPY_TESTING") { + // in tests or when debugging, enforce the "tidy" lint checks + noticef = logger.Panicf + } + + // plug/slot sanitization not used nor possible from snap command, make it no-op + snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {} +} + +var ( + // Standard streams, redirected for testing. + Stdin io.Reader = os.Stdin + Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + // overridden for testing + ReadPassword = terminal.ReadPassword + // set to logger.Panicf in testing + noticef = logger.Noticef +) + +type options struct { + Version func() `long:"version"` +} + +type argDesc struct { + name string + desc string +} + +var optionsData options + +// ErrExtraArgs is returned if extra arguments to a command are found +var ErrExtraArgs = fmt.Errorf(i18n.G("too many arguments for command")) + +// cmdInfo holds information needed to call parser.AddCommand(...). +type cmdInfo struct { + name, shortHelp, longHelp string + builder func() flags.Commander + hidden bool + optDescs map[string]string + argDescs []argDesc + alias string +} + +// commands holds information about all non-debug commands. +var commands []*cmdInfo + +// debugCommands holds information about all debug commands. +var debugCommands []*cmdInfo + +// addCommand replaces parser.addCommand() in a way that is compatible with +// re-constructing a pristine parser. +func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { + info := &cmdInfo{ + name: name, + shortHelp: shortHelp, + longHelp: longHelp, + builder: builder, + optDescs: optDescs, + argDescs: argDescs, + } + commands = append(commands, info) + return info +} + +// addDebugCommand replaces parser.addCommand() in a way that is +// compatible with re-constructing a pristine parser. It is meant for +// adding debug commands. +func addDebugCommand(name, shortHelp, longHelp string, builder func() flags.Commander) *cmdInfo { + info := &cmdInfo{ + name: name, + shortHelp: shortHelp, + longHelp: longHelp, + builder: builder, + } + debugCommands = append(debugCommands, info) + return info +} + +type parserSetter interface { + setParser(*flags.Parser) +} + +func lintDesc(cmdName, optName, desc, origDesc string) { + if len(optName) == 0 { + logger.Panicf("option on %q has no name", cmdName) + } + if len(origDesc) != 0 { + logger.Panicf("description of %s's %q of %q set from tag (=> no i18n)", cmdName, optName, origDesc) + } + if len(desc) > 0 { + // decode the first rune instead of converting all of desc into []rune + r, _ := utf8.DecodeRuneInString(desc) + // note IsLower != !IsUpper for runes with no upper/lower. + // Also note that login.u.c. is the only exception we're allowing for + // now, but the list of exceptions could grow -- if it does, we might + // want to change it to check for urlish things instead of just + // login.u.c. + if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") { + noticef("description of %s's %q is lowercase: %q", cmdName, optName, desc) + } + } +} + +func lintArg(cmdName, optName, desc, origDesc string) { + lintDesc(cmdName, optName, desc, origDesc) + if optName[0] != '<' || optName[len(optName)-1] != '>' { + noticef("argument %q's %q should be wrapped in <>s", cmdName, optName) + } +} + +// Parser creates and populates a fresh parser. +// Since commands have local state a fresh parser is required to isolate tests +// from each other. +func Parser() *flags.Parser { + optionsData.Version = func() { + printVersions() + panic(&exitStatus{0}) + } + parser := flags.NewParser(&optionsData, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) + parser.ShortDescription = i18n.G("Tool to interact with snaps") + parser.LongDescription = i18n.G(` +Install, configure, refresh and remove snap packages. Snaps are +'universal' packages that work across many different Linux systems, +enabling secure distribution of the latest apps and utilities for +cloud, servers, desktops and the internet of things. + +This is the CLI for snapd, a background service that takes care of +snaps on the system. Start with 'snap list' to see installed snaps. +`) + parser.FindOptionByLongName("version").Description = i18n.G("Print the version and exit") + + // Add all regular commands + for _, c := range commands { + obj := c.builder() + if x, ok := obj.(parserSetter); ok { + x.setParser(parser) + } + + cmd, err := parser.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj) + if err != nil { + logger.Panicf("cannot add command %q: %v", c.name, err) + } + cmd.Hidden = c.hidden + if c.alias != "" { + cmd.Aliases = append(cmd.Aliases, c.alias) + } + + opts := cmd.Options() + if c.optDescs != nil && len(opts) != len(c.optDescs) { + logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs)) + } + for _, opt := range opts { + name := opt.LongName + if name == "" { + name = string(opt.ShortName) + } + desc, ok := c.optDescs[name] + if !(c.optDescs == nil || ok) { + logger.Panicf("%s missing description for %s", c.name, name) + } + lintDesc(c.name, name, desc, opt.Description) + if desc != "" { + opt.Description = desc + } + } + + args := cmd.Args() + if c.argDescs != nil && len(args) != len(c.argDescs) { + logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs)) + } + for i, arg := range args { + name, desc := arg.Name, "" + if c.argDescs != nil { + name = c.argDescs[i].name + desc = c.argDescs[i].desc + } + lintArg(c.name, name, desc, arg.Description) + arg.Name = name + arg.Description = desc + } + } + // Add the debug command + debugCommand, err := parser.AddCommand("debug", shortDebugHelp, longDebugHelp, &cmdDebug{}) + debugCommand.Hidden = true + if err != nil { + logger.Panicf("cannot add command %q: %v", "debug", err) + } + // Add all the sub-commands of the debug command + for _, c := range debugCommands { + cmd, err := debugCommand.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), c.builder()) + if err != nil { + logger.Panicf("cannot add debug command %q: %v", c.name, err) + } + cmd.Hidden = c.hidden + } + return parser +} + +// ClientConfig is the configuration of the Client used by all commands. +var ClientConfig = client.Config{ + // we need the powerful snapd socket + Socket: dirs.SnapdSocket, + // Allow interactivity if we have a terminal + Interactive: terminal.IsTerminal(0), +} + +// Client returns a new client using ClientConfig as configuration. +func Client() *client.Client { + return client.New(&ClientConfig) +} + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(Stderr, i18n.G("WARNING: failed to activate logging: %v\n"), err) + } +} + +func resolveApp(snapApp string) (string, error) { + target, err := os.Readlink(filepath.Join(dirs.SnapBinariesDir, snapApp)) + if err != nil { + return "", err + } + if filepath.Base(target) == target { // alias pointing to an app command in /snap/bin + return target, nil + } + return snapApp, nil +} + +func main() { + cmd.ExecInCoreSnap() + + // magic \o/ + snapApp := filepath.Base(os.Args[0]) + if osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, snapApp)) { + var err error + snapApp, err = resolveApp(snapApp) + if err != nil { + fmt.Fprintf(Stderr, i18n.G("cannot resolve snap app %q: %v"), snapApp, err) + os.Exit(46) + } + cmd := &cmdRun{} + args := []string{snapApp} + args = append(args, os.Args[1:]...) + // this will call syscall.Exec() so it does not return + // *unless* there is an error, i.e. we setup a wrong + // symlink (or syscall.Exec() fails for strange reasons) + err = cmd.Execute(args) + fmt.Fprintf(Stderr, i18n.G("internal error, please report: running %q failed: %v\n"), snapApp, err) + os.Exit(46) + } + + defer func() { + if v := recover(); v != nil { + if e, ok := v.(*exitStatus); ok { + os.Exit(e.code) + } + panic(v) + } + }() + + // no magic /o\ + if err := run(); err != nil { + fmt.Fprintf(Stderr, errorPrefix, err) + os.Exit(1) + } +} + +type exitStatus struct { + code int +} + +func (e *exitStatus) Error() string { + return fmt.Sprintf("internal error: exitStatus{%d} being handled as normal error", e.code) +} + +func run() error { + parser := Parser() + _, err := parser.Parse() + if err != nil { + if e, ok := err.(*flags.Error); ok { + if e.Type == flags.ErrHelp || e.Type == flags.ErrCommandRequired { + if parser.Command.Active != nil && parser.Command.Active.Name == "help" { + parser.Command.Active = nil + } + parser.WriteHelp(Stdout) + return nil + } + if e.Type == flags.ErrUnknownCommand { + return fmt.Errorf(i18n.G(`unknown command %q, see "snap --help"`), os.Args[1]) + } + } + + msg, err := errorToCmdMessage("", err, nil) + if err != nil { + return err + } + + fmt.Fprintf(Stderr, msg) + } + + return nil +} diff --git a/cmd/snap/main_test.go b/cmd/snap/main_test.go new file mode 100644 index 00000000..3fddd9e7 --- /dev/null +++ b/cmd/snap/main_test.go @@ -0,0 +1,283 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + snapdsnap "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type BaseSnapSuite struct { + testutil.BaseTest + stdin *bytes.Buffer + stdout *bytes.Buffer + stderr *bytes.Buffer + password string + + AuthFile string +} + +func (s *BaseSnapSuite) readPassword(fd int) ([]byte, error) { + return []byte(s.password), nil +} + +func (s *BaseSnapSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + dirs.SetRootDir(c.MkDir()) + + s.stdin = bytes.NewBuffer(nil) + s.stdout = bytes.NewBuffer(nil) + s.stderr = bytes.NewBuffer(nil) + s.password = "" + + snap.Stdin = s.stdin + snap.Stdout = s.stdout + snap.Stderr = s.stderr + snap.ReadPassword = s.readPassword + s.AuthFile = filepath.Join(c.MkDir(), "json") + os.Setenv(TestAuthFileEnvKey, s.AuthFile) + + snapdsnap.MockSanitizePlugsSlots(func(snapInfo *snapdsnap.Info) {}) +} + +func (s *BaseSnapSuite) TearDownTest(c *C) { + snap.Stdin = os.Stdin + snap.Stdout = os.Stdout + snap.Stderr = os.Stderr + snap.ReadPassword = terminal.ReadPassword + + c.Assert(s.AuthFile == "", Equals, false) + err := os.Unsetenv(TestAuthFileEnvKey) + c.Assert(err, IsNil) + dirs.SetRootDir("/") + s.BaseTest.TearDownTest(c) +} + +func (s *BaseSnapSuite) Stdout() string { + return s.stdout.String() +} + +func (s *BaseSnapSuite) Stderr() string { + return s.stderr.String() +} + +func (s *BaseSnapSuite) ResetStdStreams() { + s.stdin.Reset() + s.stdout.Reset() + s.stderr.Reset() +} + +func (s *BaseSnapSuite) RedirectClientToTestServer(handler func(http.ResponseWriter, *http.Request)) { + server := httptest.NewServer(http.HandlerFunc(handler)) + s.BaseTest.AddCleanup(func() { server.Close() }) + snap.ClientConfig.BaseURL = server.URL + s.BaseTest.AddCleanup(func() { snap.ClientConfig.BaseURL = "" }) +} + +func (s *BaseSnapSuite) Login(c *C) { + err := osutil.AtomicWriteFile(s.AuthFile, []byte(TestAuthFileContents), 0600, 0) + c.Assert(err, IsNil) +} + +func (s *BaseSnapSuite) Logout(c *C) { + if osutil.FileExists(s.AuthFile) { + c.Assert(os.Remove(s.AuthFile), IsNil) + } +} + +type SnapSuite struct { + BaseSnapSuite +} + +var _ = Suite(&SnapSuite{}) + +// DecodedRequestBody returns the JSON-decoded body of the request. +func DecodedRequestBody(c *C, r *http.Request) map[string]interface{} { + var body map[string]interface{} + decoder := json.NewDecoder(r.Body) + decoder.UseNumber() + err := decoder.Decode(&body) + c.Assert(err, IsNil) + return body +} + +// EncodeResponseBody writes JSON-serialized body to the response writer. +func EncodeResponseBody(c *C, w http.ResponseWriter, body interface{}) { + encoder := json.NewEncoder(w) + err := encoder.Encode(body) + c.Assert(err, IsNil) +} + +func mockArgs(args ...string) (restore func()) { + old := os.Args + os.Args = args + return func() { os.Args = old } +} + +func mockVersion(v string) (restore func()) { + old := cmd.Version + cmd.Version = v + return func() { cmd.Version = old } +} + +func mockSnapConfine(libExecDir string) func() { + snapConfine := filepath.Join(libExecDir, "snap-confine") + if err := os.MkdirAll(libExecDir, 0755); err != nil { + panic(err) + } + if err := ioutil.WriteFile(snapConfine, nil, 0644); err != nil { + panic(err) + } + return func() { + if err := os.Remove(snapConfine); err != nil { + panic(err) + } + } +} + +const TestAuthFileEnvKey = "SNAPD_AUTH_DATA_FILENAME" +const TestAuthFileContents = `{"id":123,"email":"hello@mail.com","macaroon":"MDAxM2xvY2F0aW9uIHNuYXBkCjAwMTJpZGVudGlmaWVyIDQzCjAwMmZzaWduYXR1cmUg5RfMua72uYop4t3cPOBmGUuaoRmoDH1HV62nMJq7eqAK"}` + +func (s *SnapSuite) TestErrorResult(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "cannot do something"}}`) + }) + + restore := mockArgs("snap", "install", "foo") + defer restore() + + err := snap.RunMain() + c.Assert(err, ErrorMatches, `cannot do something`) +} + +func (s *SnapSuite) TestAccessDeniedHint(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "error", "result": {"message": "access denied", "kind": "login-required"}, "status-code": 401}`) + }) + + restore := mockArgs("snap", "install", "foo") + defer restore() + + err := snap.RunMain() + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, `access denied (try with sudo)`) +} + +func (s *SnapSuite) TestExtraArgs(c *C) { + restore := mockArgs("snap", "abort", "1", "xxx", "zzz") + defer restore() + + err := snap.RunMain() + c.Assert(err, ErrorMatches, `too many arguments for command`) +} + +func (s *SnapSuite) TestVersionOnClassic(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "--version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + c.Assert(func() { snap.RunMain() }, PanicMatches, `internal error: exitStatus\{0\} .*`) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\nubuntu 12.34\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestVersionOnAllSnap(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + }) + restore := mockArgs("snap", "--version") + defer restore() + restore = mockVersion("4.56") + defer restore() + + c.Assert(func() { snap.RunMain() }, PanicMatches, `internal error: exitStatus\{0\} .*`) + c.Assert(s.Stdout(), Equals, "snap 4.56\nsnapd 7.89\nseries 56\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestUnknownCommand(c *C) { + restore := mockArgs("snap", "unknowncmd") + defer restore() + + err := snap.RunMain() + c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see "snap --help"`) +} + +func (s *SnapSuite) TestResolveApp(c *C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, IsNil) + + // "wrapper" symlinks + err = os.Symlink("/usr/bin/snap", filepath.Join(dirs.SnapBinariesDir, "foo")) + c.Assert(err, IsNil) + err = os.Symlink("/usr/bin/snap", filepath.Join(dirs.SnapBinariesDir, "foo.bar")) + c.Assert(err, IsNil) + + // alias symlinks + err = os.Symlink("foo", filepath.Join(dirs.SnapBinariesDir, "foo_")) + c.Assert(err, IsNil) + err = os.Symlink("foo.bar", filepath.Join(dirs.SnapBinariesDir, "foo_bar-1")) + c.Assert(err, IsNil) + + snapApp, err := snap.ResolveApp("foo") + c.Assert(err, IsNil) + c.Check(snapApp, Equals, "foo") + + snapApp, err = snap.ResolveApp("foo.bar") + c.Assert(err, IsNil) + c.Check(snapApp, Equals, "foo.bar") + + snapApp, err = snap.ResolveApp("foo_") + c.Assert(err, IsNil) + c.Check(snapApp, Equals, "foo") + + snapApp, err = snap.ResolveApp("foo_bar-1") + c.Assert(err, IsNil) + c.Check(snapApp, Equals, "foo.bar") + + _, err = snap.ResolveApp("baz") + c.Check(err, NotNil) +} diff --git a/cmd/snap/notes.go b/cmd/snap/notes.go new file mode 100644 index 00000000..50dc2733 --- /dev/null +++ b/cmd/snap/notes.go @@ -0,0 +1,169 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" +) + +func getPriceString(prices map[string]float64, suggestedCurrency, status string) string { + price, currency, err := getPrice(prices, suggestedCurrency) + + // If there are no prices, then the snap is free + if err != nil { + return "" + } + + // If the snap is priced, but has been purchased + if status == "available" { + return i18n.G("bought") + } + + return formatPrice(price, currency) +} + +func formatPrice(val float64, currency string) string { + return fmt.Sprintf("%.2f%s", val, currency) +} + +// Notes encapsulate everything that might be interesting about a +// snap, in order to present a brief summary of it. +type Notes struct { + Price string + SnapType snap.Type + Private bool + DevMode bool + JailMode bool + Classic bool + TryMode bool + Disabled bool + Broken bool + IgnoreValidation bool +} + +func NotesFromChannelSnapInfo(ref *snap.ChannelSnapInfo) *Notes { + return &Notes{ + DevMode: ref.Confinement == client.DevModeConfinement, + Classic: ref.Confinement == client.ClassicConfinement, + } +} + +func NotesFromRemote(snp *client.Snap, resInfo *client.ResultInfo) *Notes { + notes := &Notes{ + Private: snp.Private, + DevMode: snp.Confinement == client.DevModeConfinement, + Classic: snp.Confinement == client.ClassicConfinement, + SnapType: snap.Type(snp.Type), + } + if resInfo != nil { + notes.Price = getPriceString(snp.Prices, resInfo.SuggestedCurrency, snp.Status) + } + + return notes +} + +func NotesFromLocal(snp *client.Snap) *Notes { + return &Notes{ + SnapType: snap.Type(snp.Type), + Private: snp.Private, + DevMode: snp.DevMode, + Classic: !snp.JailMode && (snp.Confinement == client.ClassicConfinement), + JailMode: snp.JailMode, + TryMode: snp.TryMode, + Disabled: snp.Status != client.StatusActive, + Broken: snp.Broken != "", + IgnoreValidation: snp.IgnoreValidation, + } +} + +func NotesFromInfo(info *snap.Info) *Notes { + return &Notes{ + SnapType: info.Type, + Private: info.Private, + DevMode: info.Confinement == client.DevModeConfinement, + Classic: info.Confinement == client.ClassicConfinement, + Broken: info.Broken != "", + } +} + +func (n *Notes) String() string { + if n == nil { + return "" + } + var ns []string + + switch n.SnapType { + case "", snap.TypeApp: + // nothing + case snap.TypeOS: + ns = append(ns, "core") + default: + ns = append(ns, string(n.SnapType)) + } + if n.Disabled { + // TRANSLATORS: if possible, a single short word + ns = append(ns, i18n.G("disabled")) + } + + if n.Price != "" { + ns = append(ns, n.Price) + } + + if n.DevMode { + ns = append(ns, "devmode") + } + + if n.JailMode { + ns = append(ns, "jailmode") + } + + if n.Classic { + ns = append(ns, "classic") + } + + if n.Private { + // TRANSLATORS: if possible, a single short word + ns = append(ns, i18n.G("private")) + } + + if n.TryMode { + ns = append(ns, "try") + } + + if n.Broken { + // TRANSLATORS: if possible, a single short word + ns = append(ns, i18n.G("broken")) + } + + if n.IgnoreValidation { + ns = append(ns, i18n.G("ignore-validation")) + } + + if len(ns) == 0 { + return "-" + } + + return strings.Join(ns, ",") +} diff --git a/cmd/snap/notes_test.go b/cmd/snap/notes_test.go new file mode 100644 index 00000000..acf0addd --- /dev/null +++ b/cmd/snap/notes_test.go @@ -0,0 +1,107 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main_test + +import ( + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +type notesSuite struct{} + +var _ = check.Suite(¬esSuite{}) + +func (notesSuite) TestNoNotes(c *check.C) { + c.Check((&snap.Notes{}).String(), check.Equals, "-") +} + +func (notesSuite) TestNotesPrice(c *check.C) { + c.Check((&snap.Notes{ + Price: "3.50GBP", + }).String(), check.Equals, "3.50GBP") +} + +func (notesSuite) TestNotesPrivate(c *check.C) { + c.Check((&snap.Notes{ + Private: true, + }).String(), check.Equals, "private") +} + +func (notesSuite) TestNotesDevMode(c *check.C) { + c.Check((&snap.Notes{ + DevMode: true, + }).String(), check.Equals, "devmode") +} + +func (notesSuite) TestNotesJailMode(c *check.C) { + c.Check((&snap.Notes{ + JailMode: true, + }).String(), check.Equals, "jailmode") +} + +func (notesSuite) TestNotesClassic(c *check.C) { + c.Check((&snap.Notes{ + Classic: true, + }).String(), check.Equals, "classic") +} + +func (notesSuite) TestNotesTryMode(c *check.C) { + c.Check((&snap.Notes{ + TryMode: true, + }).String(), check.Equals, "try") +} + +func (notesSuite) TestNotesDisabled(c *check.C) { + c.Check((&snap.Notes{ + Disabled: true, + }).String(), check.Equals, "disabled") +} + +func (notesSuite) TestNotesBroken(c *check.C) { + c.Check((&snap.Notes{ + Broken: true, + }).String(), check.Equals, "broken") +} + +func (notesSuite) TestNotesIgnoreValidation(c *check.C) { + c.Check((&snap.Notes{ + IgnoreValidation: true, + }).String(), check.Equals, "ignore-validation") +} + +func (notesSuite) TestNotesNothing(c *check.C) { + c.Check((&snap.Notes{}).String(), check.Equals, "-") +} + +func (notesSuite) TestNotesTwo(c *check.C) { + c.Check((&snap.Notes{ + DevMode: true, + Broken: true, + }).String(), check.Matches, "(devmode,broken|broken,devmode)") +} + +func (notesSuite) TestNotesFromLocal(c *check.C) { + // Check that DevMode note is derived from DevMode flag, not DevModeConfinement type. + c.Check(snap.NotesFromLocal(&client.Snap{DevMode: true}).DevMode, check.Equals, true) + c.Check(snap.NotesFromLocal(&client.Snap{Confinement: client.DevModeConfinement}).DevMode, check.Equals, false) + c.Check(snap.NotesFromLocal(&client.Snap{IgnoreValidation: true}).IgnoreValidation, check.Equals, true) +} diff --git a/cmd/snap/test-data/pubring.gpg b/cmd/snap/test-data/pubring.gpg new file mode 100644 index 0000000000000000000000000000000000000000..8d2ff84efa28a80060d89d06510353284f1b1586 GIT binary patch literal 2192 zcmV;B2yge90u2OKg~5OU5CGpj2dm$GVDv5FI2VnFcwnj{yN3HcunTm+4X@Iob=oYa zmnB_xtu-5boyXHdt=uDOh$5AYj-uVI-8I}nx_Ng#cmVt-@TKYi>W|94!NckFtedK2 z5RO@uM%MkXJN)~-^K%QLt(2C_{tqA_$aVU6_wWI zdAPe3mYUk;LmFlTsI&$=0~Fq1yilBJ`9fUEew9n-_>^gWf_4Jg_%Q$s6gb$y@!VUfm-p@q2oKuzlDf%&t~2L zGm0NH)+Letv0G_C^LI<^M}CCVK8pktRvz=5xcBq%U$3DV35^1E*_FN!H43$%hea-M zfa6oOoHOP_U#&|3j@w)F3=^=|#bE)_1GP8>tanG9aD$B`4!FtzSg*)#> z3)}U*n??iSQC(v`iujItJHW!2AQr=n&pS zO(-Ji25ZN~ti~6ab7n~##YP9`i z<>}Jd#9+NQ2-0T=w>!7%xd^^r4!0+eIG++2%>OktjX;`25}pj|^L-ddn@eKdf_e%g z>Boin9C!@@)OK@RnF2g8n+?*{k`>7d!H$|;+{EMPLZTOU*H#EQI*`zYiH%!r!KFFt z6}Qn*XTra*0ssS<0u2OKg~5OU5CHH|31}uG_zZPT$lF&FL7r^RypzqUit(5qcT&}) zp5Yi(l14nxh0hgq3&{?c-V>{9ktsBIwR02o&{lWP_QRg{s_2IDsh8SFI5tAV?u)wlo7vF6JE$@?o$m@X{gxD}$Zb5QNpJD3hMD zhDa!>bugP%C=w6bMXK3)ugAXdzq5XiHA=eARx{L}vM6PsUOGSsb_Q1w*5&4I@~}G=V4+^U)c+ zM~(>3MJvehIVVfjK&-|06`i4l^7xxon=(4gmzG1Vecs=QhMtm*R0$tcH=QL>Te zd3_Ql3>}d6>l_ZGQGoLUcx=KtOZ>O@<35Y|HE}ca+XPW-B|JbxVpgT9EjmneYub=kN>qYviMxFi7w|n7PSsxv1NIY?CW|94!NckFtedK2 z5RO@uM%MkXJN)~-^K%QLt(2C_{tqA_$aVU6_wWI zdAPe3mYUk;LmFlTsI&$=0~Fq1yilBJ`9fUEew9n-_>^gWf_4Jg_%Q$s6gb$^rj`Ay}eZCiL0DfeXLseOZ0RcyoLVUAK#5?}(_ z=J8l`TF9M?D7ccr+r9agI)I2PcG@ zu5_}LN^EIzmcZ(_* z+9<0uU^_ER8eqXZBAjmXgQc#<-24BH{Cpn@2V3gWS9J?cvMkXjGZ$W*Qf{>e_-}RW zV@nNb{$MwiM)<;JdgX0vP6yn@O`HddKfBY>r3>hd~#*N z4dB*K+KZje!8c@aMwPpqp4xx`q=R}#ypwZXs{f3wym{a^NM|jf&61Tc2}xTA@5Fvn zm8-#`6rtHXLNW;hJ;=t1WZS}>ZJtaQz$ZsDRMHy1B%R3vCPnwJ^jGp=6_1F55pBMwyr&VzdxZAxP+%G8k| z+%vBt!jzHSAz_72&+E)Qj4k0BQgD0s8_$zj7+%{KdbJadl;HZOmy>q#rTZ*pz4LAg z*9329jx%RSL1@adU`%4^H}aNaA0a8}H6AoUmV>7Ub%FsjZbm9^R#g>LIe#!6JvK>J z6Sm+=jYcNn2mtwB{X`TMyWRZQ0vlM`P*S)c^$o()lU6V~FLSid?Jx#ae+s>HVi~ zghus=NrTCo3Ty{*XA=pl>Rr~mb6T{emJ+i4ZvKG3s7d*`z*xYL= zWI3l!B4l|>of9>g*{NCEyM{SMnmx5O+{CCr3mwH`{c2y$kiqf&AI3(vzy}SztzIh? z6}yK#y+Js~pfVS&SH8*J1d%uw5gVt1UNan-IUW;7y_Y~mYxy_&9;1fCO5 z+b_G!P&z6Erm{_}Gk&0J(cU3~gFaG?^d=wgay8=+wnIv%C=#osr3^xDb&w*@(8Utz zbI;MWAd-|VKBkRGcfy?2L)`D_qg|}T8xS%eS%SVk*x&G#>*7o3XigRBzWH21eszqR z`lm6!b(XXTWMyVyb!>Er0w@F%0RjLL1p-!u!GHoAF9H<`0v-VZ7k~f?2@v!2zuI+F zRg_Zt5CFliCsnry&wS^{_Q;lYvB%+(bS-x8A+s^%Ha*TV%LUc@54ogxqndiE9e>Yf zaBz-KYZO#Zg^7{DKTUO8zPbr3)pp>SF!87*2)-@Yrtl*YXdM(yq8GGU`1^1`w^jz;S@rN#*m**6x8`l+86oFo!zwEbk|>C)N6V7)g8 z(q{;_JGbk(2)@!GHk}0Ps->XeJ~040TP&+gB4oo@~y%lg+A%@t7ZXQq`oM;TTntMm*4k z&lPkF$qty_6RT^HDKvPsa})K@R(H?#!=Cr5@L_(2WH`Po5AI$IX@w9ief(^66=U_E zRLAf*fhf>dtq~z0NFimmGy_X6<{b|5VX!Ok(jovWgPqtAgw?1hlb*7MNGPdwFq>5< z5)a!)A|!__Sf!ifT1Ei`4tYwDy`Cs^Kk(_zKpx%~kieQdM^a#wVtG#CA_5vc+$?$F zgpP!uD@`^`2kY!bmkgt}10r}JBQ`Z?V~Rnx>MZ^`iMwmXswgGoF2yOnAJ=PYdYkAA z*#CQIAzR95OHT59)qd4PX8B=H#!>xQ9JlNRL!+w=BUs8bfhZI6(HXo)jtLdFhKgUK z!OG0bR2WhOP2idwXZ1o6Q1L4F!l5xEW}UBMhD(d(T3lgB{Hp{DIr%dD7uLva1hpD> zKeVbJ*4Nadej7JElfIBXY8-k65?C1F1daT;wWu!U(8vqdj0096G`vtO|D*AgcE8-l-E-)k1JxSQjq#=2+ex>Gx7LhQ4)z%dU`A!1S zrm%6b6)eN-!B+Q57@sS$C{EL@Sh6PDo@%@HF%*GU^>MuGT(K3X5%E9<}6g?IIG~UCCNnB~r1N9hIQ#GU|0Na2>|z`-06aH9Diu%A=u6aLZdX8kH}#cj^E28V-O0*8}O>8fsx2YvIy2ZwahePgh2H8s5$g@9=klGoG=A?#0SJWz?i zg2^hg?Xf-RI;gHCSmVaOkHu7$?erRz)nH4}J@_lnJfNqzeGI(6W`bIj5S!i)m7@N4 zz{p0du?PV6@@}x(EX#*i$>zTqgyO}Uyo7v1m(uPu!SA>_;L~t|ajhLdAlLJ+9-Gko*SvzaD9U=$*}7|ME+7> zvs<0mY*CAkDG7Z;PLc?Nd~|1AZM&f&a3V4K7P}fg931(?sCiGEG7CXzwC|tgg|ipI z8v0WB>W1T0XcGHJ{`iRFNo8?aGJE3F1bL`3D?JkvC}%#&n{DfeEwuic(;VQ-qg4f_ zVE%Ck0Qn$b-819joPLV57(=i&{xUV_4OR5f(A97sB}F)Tu(~AVqYRc4)Z1SYZ&%XB zL1_i=uep(-K=w|0B{m)mvA#V|yk*5mn%q&3hr@Omb1Ea!TKorzG7a^?G5sv^H8~ex zBWURE948-kuT;p1Rzi~9sVBKE6hFaB0l-;XJb9YIJe+~=LbAp4B=!+6U7xQ$m%JeI z#lqF#a71|8{*QbQG)w$E(K7r(MS&y=2jE9PUp2*lNWI%ep(;UaON(?~$(887nzlQG z+EUfwwN=Y$IC$!%x``_ri%qks8P)%7`e0Z)yAezyNg&q!MVdEp^N$ZXl+z-Vh5rv` zO9%gCTVkEzJS`}IqVYAI$rH{SsL0iyd-+AHWE3T=L4%hG8D%@WQJwsZ2(!$7RdlqQ zfY$DgaHkOb^=j1{%GwJMw@VLz&$u?&kp`3Ms{_==@~P=IelG+K-j12K z+q4H^Zf|sGWparEC4Z9svRufB*^!5WnZHe?EEgWk}T! z0Fmi=a1ee8v*Up|I;a05M8pm=95cCWp3@Ei1g8W?8~=B7$z~YQb7R;>u%$J`h#RhR z?yc>O#<`mP4Ti~cA_6Cy-x4)S@}*f5a(z4*E{9~wuB8*qDS=k^iPr9++*L0ar?MFk zqRI?G-5`D5--w2ul8saeA5=G;HdS_O*EkxM`yJ{h_sDsD5+)2CkoM~w4x~|l^89QxUVL%K_}!FSQU_tF8ACvMWvc2(!@mF6v^=0Y zH>Hirli;tP_R#A^`l?2q{m-|1;aOQ99cxHDackuZ&5)FvTrk#;%!jfqJpvHCN&xg+ zWO0MJl&j-Y@nTY;<ElUVGdm><{9 literal 0 HcmV?d00001 diff --git a/cmd/snap/test-data/trustdb.gpg b/cmd/snap/test-data/trustdb.gpg new file mode 100644 index 0000000000000000000000000000000000000000..2f7c9ad58b3d9106251903290e20599dc1095e2d GIT binary patch literal 1360 zcmZQfFGy!*W@Ke#VqgeApl`*19WZiX7sn7CRfiEIV1dza87AQ(H%$D2-5U(xbSjTS z5=nTV(lZakI=*W?Pd85&s(LTX`1#BJTcshPQ`jIbQ$#qBhvDHehRXGfn^(P_w)fip z=j-b2DnF${)hQs<@iOG(%rkz!Lf8BeFMq(C9q-!t@0zTGszWGa;AObrY&iXLQ05~M SPTw%L{<1F7Pan82)d2w2$Sw2$ literal 0 HcmV?d00001 diff --git a/cmd/snapctl/main.go b/cmd/snapctl/main.go new file mode 100644 index 00000000..bf716343 --- /dev/null +++ b/cmd/snapctl/main.go @@ -0,0 +1,78 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" +) + +var clientConfig = client.Config{ + // snapctl should not try to read $HOME/.snap/auth.json, this will + // result in apparmor denials and configure task failures + // (LP: #1660941) + DisableAuth: true, + + // we need the less privileged snap socket in snapctl + Socket: dirs.SnapSocket, +} + +func main() { + // check for internal commands + if len(os.Args) > 2 && os.Args[1] == "internal" { + switch os.Args[2] { + case "configure-core": + fmt.Fprintf(os.Stderr, "no internal core configuration anymore") + os.Exit(1) + } + } + + // no internal command, route via snapd + stdout, stderr, err := run() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %s\n", err) + os.Exit(1) + } + + if stdout != nil { + os.Stdout.Write(stdout) + } + + if stderr != nil { + os.Stderr.Write(stderr) + } +} + +func run() (stdout, stderr []byte, err error) { + cli := client.New(&clientConfig) + + cookie := os.Getenv("SNAP_COOKIE") + // for compatibility, if re-exec is not enabled and facing older snapd. + if cookie == "" { + cookie = os.Getenv("SNAP_CONTEXT") + } + return cli.RunSnapctl(&client.SnapCtlOptions{ + ContextID: cookie, + Args: os.Args[1:], + }) +} diff --git a/cmd/snapctl/main_test.go b/cmd/snapctl/main_test.go new file mode 100644 index 00000000..380ba55d --- /dev/null +++ b/cmd/snapctl/main_test.go @@ -0,0 +1,117 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/snapcore/snapd/client" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type snapctlSuite struct { + server *httptest.Server + oldArgs []string + expectedContextID string + expectedArgs []string +} + +var _ = Suite(&snapctlSuite{}) + +func (s *snapctlSuite) SetUpTest(c *C) { + os.Setenv("SNAP_COOKIE", "snap-context-test") + n := 0 + s.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Assert(r.Method, Equals, "POST") + c.Assert(r.URL.Path, Equals, "/v2/snapctl") + c.Assert(r.Header.Get("Authorization"), Equals, "") + + var snapctlOptions client.SnapCtlOptions + decoder := json.NewDecoder(r.Body) + c.Assert(decoder.Decode(&snapctlOptions), IsNil) + c.Assert(snapctlOptions.ContextID, Equals, s.expectedContextID) + c.Assert(snapctlOptions.Args, DeepEquals, s.expectedArgs) + + fmt.Fprintln(w, `{"type": "sync", "result": {"stdout": "test stdout", "stderr": "test stderr"}}`) + default: + c.Fatalf("expected to get 1 request, now on %d", n+1) + } + + n++ + })) + clientConfig.BaseURL = s.server.URL + s.oldArgs = os.Args + os.Args = []string{"snapctl"} + s.expectedContextID = "snap-context-test" + s.expectedArgs = []string{} + + fakeAuthPath := filepath.Join(c.MkDir(), "auth.json") + os.Setenv("SNAPD_AUTH_DATA_FILENAME", fakeAuthPath) + err := ioutil.WriteFile(fakeAuthPath, []byte(`{"macaroon":"user-macaroon"}`), 0644) + c.Assert(err, IsNil) +} + +func (s *snapctlSuite) TearDownTest(c *C) { + os.Unsetenv("SNAP_COOKIE") + os.Unsetenv("SNAPD_AUTH_DATA_FILENAME") + clientConfig.BaseURL = "" + s.server.Close() + os.Args = s.oldArgs +} + +func (s *snapctlSuite) TestSnapctl(c *C) { + stdout, stderr, err := run() + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "test stdout") + c.Check(string(stderr), Equals, "test stderr") +} + +func (s *snapctlSuite) TestSnapctlWithArgs(c *C) { + os.Args = []string{"snapctl", "foo", "--bar"} + + s.expectedArgs = []string{"foo", "--bar"} + stdout, stderr, err := run() + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "test stdout") + c.Check(string(stderr), Equals, "test stderr") +} + +func (s *snapctlSuite) TestSnapctlHelp(c *C) { + os.Unsetenv("SNAP_COOKIE") + s.expectedContextID = "" + + os.Args = []string{"snapctl", "-h"} + s.expectedArgs = []string{"-h"} + + _, _, err := run() + c.Check(err, IsNil) +} diff --git a/cmd/snapd/main.go b/cmd/snapd/main.go new file mode 100644 index 00000000..f01ae382 --- /dev/null +++ b/cmd/snapd/main.go @@ -0,0 +1,85 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/daemon" + "github.com/snapcore/snapd/errtracker" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/systemd" +) + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: failed to activate logging: %s\n", err) + } + // set here to avoid accidental submits in e.g. unit tests + errtracker.CrashDbURLBase = "https://daisy.ubuntu.com/" + errtracker.SnapdVersion = cmd.Version +} + +func main() { + cmd.ExecInCoreSnap() + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + t0 := time.Now().Truncate(time.Millisecond) + httputil.SetUserAgentFromVersion(cmd.Version) + + d, err := daemon.New() + if err != nil { + return err + } + if err := d.Init(); err != nil { + return err + } + d.Version = cmd.Version + + d.Start() + + // notify systemd that we are ready + systemd.SdNotify("READY=1") + logger.Debugf("activation done in %v", time.Now().Truncate(time.Millisecond).Sub(t0)) + + ch := make(chan os.Signal) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + select { + case sig := <-ch: + logger.Noticef("Exiting on %s signal.\n", sig) + case <-d.Dying(): + // something called Stop() + } + + systemd.SdNotify("STOPPING=1") + return d.Stop() +} diff --git a/cmd/system-shutdown/system-shutdown-utils-test.c b/cmd/system-shutdown/system-shutdown-utils-test.c new file mode 100644 index 00000000..93704621 --- /dev/null +++ b/cmd/system-shutdown/system-shutdown-utils-test.c @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "system-shutdown-utils.h" +#include "system-shutdown-utils.c" + +#include diff --git a/cmd/system-shutdown/system-shutdown-utils.c b/cmd/system-shutdown/system-shutdown-utils.c new file mode 100644 index 00000000..f46537b3 --- /dev/null +++ b/cmd/system-shutdown/system-shutdown-utils.c @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include "system-shutdown-utils.h" + +#include // errno, sys_errlist +#include // open +#include // LOOP_CLR_FD +#include +#include // va_* +#include // fprintf, stderr +#include // exit +#include // strcmp, strncmp +#include // ioctl +#include // umount +#include // reboot, RB_* +#include // mkdir +#include // getpid, close + +#include "../libsnap-confine-private/mountinfo.h" +#include "../libsnap-confine-private/string-utils.h" + +__attribute__ ((format(printf, 1, 2))) +void kmsg(const char *fmt, ...) +{ + static FILE *kmsg = NULL; + static char *head = NULL; + if (!kmsg) { + // TODO: figure out why writing to /dev/kmsg doesn't work from here + kmsg = stderr; + head = "snapd system-shutdown helper: "; + } + + va_list va; + va_start(va, fmt); + fputs(head, kmsg); + vfprintf(kmsg, fmt, va); + fprintf(kmsg, "\n"); + va_end(va); +} + +__attribute__ ((noreturn)) +void die(const char *msg) +{ + if (errno == 0) { + kmsg("*** %s", msg); + } else { + kmsg("*** %s: %s", msg, strerror(errno)); + } + sync(); + reboot(RB_HALT_SYSTEM); + exit(1); +} + +int sc_read_reboot_arg(char *arg, size_t max_size) +{ + FILE *f; + + // This file is used by systemd to pass around a reboot parameter See + // https://github.com/systemd/systemd/blob/v229/src/basic/def.h#L44 + f = fopen("/run/systemd/reboot-param", "r"); + if (!f) { + return -1; + } + + if (!fgets(arg, max_size, f)) { + fclose(f); + return -1; + } + arg[strcspn(arg, "\n")] = '\0'; + + kmsg("reboot arg is %s", arg); + fclose(f); + return 0; +} + +static void detach_loop(const char *src) +{ + int fd = open(src, O_RDONLY); + if (fd < 0) { + kmsg("* unable to open loop device %s: %s", src, + strerror(errno)); + } else { + if (ioctl(fd, LOOP_CLR_FD) < 0) { + kmsg("* unable to disassociate loop device %ss: %s", + src, strerror(errno)); + } + close(fd); + } +} + +// tries to umount all (well, most) things. Returns whether in the last pass it +// no longer found writable. +bool umount_all(void) +{ + bool did_umount = true; + bool had_writable = false; + + for (int i = 0; i < 10 && did_umount; i++) { + struct sc_mountinfo *mounts = sc_parse_mountinfo(NULL); + if (!mounts) { + // oh dear + die("unable to get mount info; giving up"); + } + struct sc_mountinfo_entry *cur = + sc_first_mountinfo_entry(mounts); + + had_writable = false; + did_umount = false; + while (cur) { + const char *dir = cur->mount_dir; + const char *src = cur->mount_source; + unsigned major = cur->dev_major; + + cur = sc_next_mountinfo_entry(cur); + + if (sc_streq("/", dir)) { + continue; + } + + if (sc_streq("/dev", dir)) { + continue; + } + + if (sc_streq("/proc", dir)) { + continue; + } + + if (major != 0 && major != LOOP_MAJOR + && sc_endswith(dir, "/writable")) { + had_writable = true; + } + + if (umount(dir) == 0) { + if (major == LOOP_MAJOR) { + detach_loop(src); + } + + did_umount = true; + } + } + sc_cleanup_mountinfo(&mounts); + } + + return !had_writable; +} diff --git a/cmd/system-shutdown/system-shutdown-utils.h b/cmd/system-shutdown/system-shutdown-utils.h new file mode 100644 index 00000000..42f0349c --- /dev/null +++ b/cmd/system-shutdown/system-shutdown-utils.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#ifndef SYSTEM_SHUTDOWN_UTILS_H +#define SYSTEM_SHUTDOWN_UTILS_H + +#include +#include // size_t + +// tries to umount all (well, most) things. Returns whether in the last pass it +// no longer found writable. +bool umount_all(void); + +__attribute__ ((noreturn)) +void die(const char *msg); +__attribute__ ((format(printf, 1, 2))) +void kmsg(const char *fmt, ...); + +// Reads a possible argument for reboot syscall in /run/systemd/reboot-param, +// which is the place where systemd stores it. +int sc_read_reboot_arg(char *arg, size_t max_size); + +#endif diff --git a/cmd/system-shutdown/system-shutdown.c b/cmd/system-shutdown/system-shutdown.c new file mode 100644 index 00000000..7e8c6064 --- /dev/null +++ b/cmd/system-shutdown/system-shutdown.c @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +#include // bools +#include // va_* +#include // umount +#include // mkdir +#include // getpid, close +#include // exit +#include // fprintf, stderr +#include // strerror +#include // ioctl +#include // LOOP_CLR_FD +#include // reboot, RB_* +#include // open +#include // errno, sys_errlist +#include // LINUX_REBOOT_MAGIC* +#include // SYS_reboot + +#include "system-shutdown-utils.h" +#include "../libsnap-confine-private/string-utils.h" + +int main(int argc, char *argv[]) +{ + // 256 should be more than enough... + char reboot_arg[256] = { 0 }; + + errno = 0; + if (getpid() != 1) { + fprintf(stderr, + "This is a shutdown helper program; don't call it directly.\n"); + exit(1); + } + + kmsg("started."); + + /* + This program is started by systemd exec'ing the "shutdown" binary + inside what used to be /run/initramfs. That is: the system's + /run/initramfs is now /, and the old / is now /oldroot. Our job is + to disentagle /oldroot and /oldroot/writable, which contain each + other in the "live" system. We do this by creating a new /writable + and moving the old mount there, previous to which we need to unmount + as much as we can. Having done that we should be able to detach the + oldroot loop device and finally unmount writable itself. + */ + + if (mkdir("/writable", 0755) < 0) { + die("cannot create directory /writable"); + } + // We are reading a file from /run and need to do this before unmounting + if (sc_read_reboot_arg(reboot_arg, sizeof reboot_arg) < 0) { + kmsg("no reboot parameter"); + } + + if (umount_all()) { + kmsg("- found no hard-to-unmount writable partition."); + } else { + if (mount("/oldroot/writable", "/writable", NULL, MS_MOVE, NULL) + < 0) { + die("cannot move writable out of the way"); + } + + bool ok = umount_all(); + kmsg("%c was %s to unmount writable cleanly", ok ? '-' : '*', + ok ? "able" : "*NOT* able"); + sync(); // shouldn't be needed, but just in case + } + + // argv[1] can be one of at least: halt, reboot, poweroff. + // FIXME: might also be kexec, hibernate or hybrid-sleep -- support those! + + int cmd = RB_HALT_SYSTEM; + + if (argc < 2) { + kmsg("* called without verb; halting."); + } else { + if (sc_streq("reboot", argv[1])) { + cmd = RB_AUTOBOOT; + kmsg("- rebooting."); + } else if (sc_streq("poweroff", argv[1])) { + cmd = RB_POWER_OFF; + kmsg("- powering off."); + } else if (sc_streq("halt", argv[1])) { + kmsg("- halting."); + } else { + kmsg("* called with unsupported verb %s; halting.", + argv[1]); + } + } + + // glibc reboot wrapper does not expose the optional reboot syscall + // parameter + + long ret; + if (cmd == RB_AUTOBOOT && reboot_arg[0] != '\0') { + ret = syscall(SYS_reboot, + LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, + LINUX_REBOOT_CMD_RESTART2, reboot_arg); + } else { + ret = reboot(cmd); + } + + if (ret == -1) { + kmsg("cannot reboot the system: %s", strerror(errno)); + } + + return 0; +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 00000000..f75f31ab --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,31 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package cmd + +//go:generate mkversion.sh + +// Version will be overwritten at build-time via mkversion.sh +var Version = "unknown" + +func MockVersion(version string) (restore func()) { + old := Version + Version = version + return func() { Version = old } +} diff --git a/corecfg/corecfg.go b/corecfg/corecfg.go new file mode 100644 index 00000000..f3b1f424 --- /dev/null +++ b/corecfg/corecfg.go @@ -0,0 +1,87 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg + +import ( + "fmt" + "os" + + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/release" +) + +var ( + Stdout = os.Stdout + Stderr = os.Stderr +) + +type Conf interface { + Get(snapName, key string, result interface{}) error + State() *state.State +} + +func coreCfg(tr Conf, key string) (result string, err error) { + var v interface{} = "" + if err := tr.Get("core", key, &v); err != nil && !config.IsNoOption(err) { + return "", err + } + // TODO: we could have a fully typed approach but at the + // moment we also always use "" to mean unset as well, this is + // the smallest change + return fmt.Sprintf("%v", v), nil +} + +func Run(tr Conf) error { + if err := validateProxyStore(tr); err != nil { + return err + } + if err := validateRefreshSchedule(tr); err != nil { + return err + } + + // see if it makes sense to run at all + if release.OnClassic { + // nothing to do + return nil + } + // TODO: consider allowing some of these on classic too? + // consider erroring on core-only options on classic? + + // handle the various core config options: + // service.*.disable + if err := handleServiceDisableConfiguration(tr); err != nil { + return err + } + // system.power-key-action + if err := handlePowerButtonConfiguration(tr); err != nil { + return err + } + // pi-config.* + if err := handlePiConfiguration(tr); err != nil { + return err + } + // proxy.{http,https,ftp} + if err := handleProxyConfiguration(tr); err != nil { + return err + } + + return nil +} diff --git a/corecfg/corecfg_test.go b/corecfg/corecfg_test.go new file mode 100644 index 00000000..e0f4db3b --- /dev/null +++ b/corecfg/corecfg_test.go @@ -0,0 +1,86 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg_test + +import ( + "fmt" + "reflect" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/systemd" +) + +func Test(t *testing.T) { TestingT(t) } + +type mockConf struct { + state *state.State + conf map[string]interface{} + err error +} + +func (cfg *mockConf) Get(snapName, key string, result interface{}) error { + if snapName != "core" { + return fmt.Errorf("mockConf only knows about core") + } + if cfg.conf[key] != nil { + v1 := reflect.ValueOf(result) + v2 := reflect.Indirect(v1) + v2.Set(reflect.ValueOf(cfg.conf[key])) + } + return cfg.err +} + +func (cfg *mockConf) State() *state.State { + return cfg.state +} + +// coreCfgSuite is the base for all the corecfg tests +type coreCfgSuite struct { + state *state.State + + systemctlArgs [][]string + systemctlRestorer func() +} + +var _ = Suite(&coreCfgSuite{}) + +func (s *coreCfgSuite) SetUpSuite(c *C) { + s.systemctlRestorer = systemd.MockSystemctl(func(args ...string) ([]byte, error) { + s.systemctlArgs = append(s.systemctlArgs, args[:]) + output := []byte("ActiveState=inactive") + return output, nil + }) +} + +func (s *coreCfgSuite) TearDownSuite(c *C) { + s.systemctlRestorer() +} + +func (s *coreCfgSuite) SetUpTest(c *C) { + s.state = state.New(nil) +} + +// runCfgSuite tests corecfg.Run() +type runCfgSuite struct { + coreCfgSuite +} diff --git a/corecfg/export_test.go b/corecfg/export_test.go new file mode 100644 index 00000000..4e655321 --- /dev/null +++ b/corecfg/export_test.go @@ -0,0 +1,27 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg + +var ( + UpdatePiConfig = updatePiConfig + SwitchHandlePowerKey = switchHandlePowerKey + SwitchDisableService = switchDisableService + UpdateKeyValueStream = updateKeyValueStream +) diff --git a/corecfg/picfg.go b/corecfg/picfg.go new file mode 100644 index 00000000..b812d651 --- /dev/null +++ b/corecfg/picfg.go @@ -0,0 +1,98 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +// valid pi config keys +var piConfigKeys = map[string]bool{ + "disable_overscan": true, + "framebuffer_width": true, + "framebuffer_height": true, + "framebuffer_depth": true, + "framebuffer_ignore_alpha": true, + "overscan_left": true, + "overscan_right": true, + "overscan_top": true, + "overscan_bottom": true, + "overscan_scale": true, + "display_rotate": true, + "hdmi_group": true, + "hdmi_mode": true, + "hdmi_drive": true, + "avoid_warnings": true, + "gpu_mem_256": true, + "gpu_mem_512": true, + "gpu_mem": true, + "sdtv_aspect": true, + "config_hdmi_boost": true, + "hdmi_force_hotplug": true, +} + +func updatePiConfig(path string, config map[string]string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + toWrite, err := updateKeyValueStream(f, piConfigKeys, config) + if err != nil { + return err + } + + if toWrite != nil { + s := strings.Join(toWrite, "\n") + return osutil.AtomicWriteFile(path, []byte(s), 0644, 0) + } + + return nil +} + +func piConfigFile() string { + return filepath.Join(dirs.GlobalRootDir, "/boot/uboot/config.txt") +} + +func handlePiConfiguration(tr Conf) error { + if osutil.FileExists(piConfigFile()) { + // snapctl can actually give us the whole dict in + // JSON, in a single call; use that instead of this. + config := map[string]string{} + for key := range piConfigKeys { + output, err := coreCfg(tr, fmt.Sprintf("pi-config.%s", strings.Replace(key, "_", "-", -1))) + if err != nil { + return err + } + config[key] = output + } + if err := updatePiConfig(piConfigFile(), config); err != nil { + return err + } + } + return nil +} diff --git a/corecfg/picfg_test.go b/corecfg/picfg_test.go new file mode 100644 index 00000000..3bae8776 --- /dev/null +++ b/corecfg/picfg_test.go @@ -0,0 +1,154 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/corecfg" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" +) + +type piCfgSuite struct { + coreCfgSuite + + mockConfigPath string +} + +var _ = Suite(&piCfgSuite{}) + +var mockConfigTxt = ` +# For more options and information see +# http://www.raspberrypi.org/documentation/configuration/config-txt.md +#hdmi_group=1 +# uncomment this if your display has a black border of unused pixels visible +# and your display can output without overscan +#disable_overscan=1 +unrelated_options=are-kept` + +func (s *piCfgSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "etc"), 0755), IsNil) + + s.mockConfigPath = filepath.Join(dirs.GlobalRootDir, "/boot/uboot/config.txt") + err := os.MkdirAll(filepath.Dir(s.mockConfigPath), 0755) + c.Assert(err, IsNil) + s.mockConfig(c, mockConfigTxt) +} + +func (s *piCfgSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +func (s *piCfgSuite) mockConfig(c *C, txt string) { + err := ioutil.WriteFile(s.mockConfigPath, []byte(txt), 0644) + c.Assert(err, IsNil) +} + +func (s *piCfgSuite) checkMockConfig(c *C, expected string) { + newContent, err := ioutil.ReadFile(s.mockConfigPath) + c.Assert(err, IsNil) + c.Check(string(newContent), Equals, expected) +} + +func (s *piCfgSuite) TestConfigurePiConfigUncommentExisting(c *C) { + err := corecfg.UpdatePiConfig(s.mockConfigPath, map[string]string{"disable_overscan": "1"}) + c.Assert(err, IsNil) + + expected := strings.Replace(mockConfigTxt, "#disable_overscan=1", "disable_overscan=1", -1) + s.checkMockConfig(c, expected) +} + +func (s *piCfgSuite) TestConfigurePiConfigCommentExisting(c *C) { + s.mockConfig(c, mockConfigTxt+"\navoid_warnings=1\n") + + err := corecfg.UpdatePiConfig(s.mockConfigPath, map[string]string{"avoid_warnings": ""}) + c.Assert(err, IsNil) + + expected := mockConfigTxt + "\n" + "#avoid_warnings=1" + s.checkMockConfig(c, expected) +} + +func (s *piCfgSuite) TestConfigurePiConfigAddNewOption(c *C) { + err := corecfg.UpdatePiConfig(s.mockConfigPath, map[string]string{"framebuffer_depth": "16"}) + c.Assert(err, IsNil) + + expected := mockConfigTxt + "\n" + "framebuffer_depth=16" + s.checkMockConfig(c, expected) + + // add again, verify its not added twice but updated + err = corecfg.UpdatePiConfig(s.mockConfigPath, map[string]string{"framebuffer_depth": "32"}) + c.Assert(err, IsNil) + expected = mockConfigTxt + "\n" + "framebuffer_depth=32" + s.checkMockConfig(c, expected) +} + +func (s *piCfgSuite) TestConfigurePiConfigNoChangeUnset(c *C) { + // ensure we cannot write to the dir to test that we really + // do not update the file + err := os.Chmod(filepath.Dir(s.mockConfigPath), 0500) + c.Assert(err, IsNil) + defer os.Chmod(filepath.Dir(s.mockConfigPath), 0755) + + err = corecfg.UpdatePiConfig(s.mockConfigPath, map[string]string{"hdmi_group": ""}) + c.Assert(err, IsNil) +} + +func (s *piCfgSuite) TestConfigurePiConfigNoChangeSet(c *C) { + // ensure we cannot write to the dir to test that we really + // do not update the file + err := os.Chmod(filepath.Dir(s.mockConfigPath), 0500) + c.Assert(err, IsNil) + defer os.Chmod(filepath.Dir(s.mockConfigPath), 0755) + + err = corecfg.UpdatePiConfig(s.mockConfigPath, map[string]string{"unrelated_options": "cannot-be-set"}) + c.Assert(err, ErrorMatches, `cannot set unsupported configuration value "unrelated_options"`) +} + +func (s *piCfgSuite) TestConfigurePiConfigIntegration(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + err := corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + "pi-config.disable-overscan": 1, + }, + }) + c.Assert(err, IsNil) + + expected := strings.Replace(mockConfigTxt, "#disable_overscan=1", "disable_overscan=1", -1) + s.checkMockConfig(c, expected) + + err = corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + "pi-config.disable-overscan": "", + }, + }) + c.Assert(err, IsNil) + + s.checkMockConfig(c, mockConfigTxt) + +} diff --git a/corecfg/powerbtn.go b/corecfg/powerbtn.go new file mode 100644 index 00000000..f0f3c2d5 --- /dev/null +++ b/corecfg/powerbtn.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +func powerBtnCfg() string { + return filepath.Join(dirs.GlobalRootDir, "/etc/systemd/logind.conf.d/00-snap-core.conf") +} + +// switchHandlePowerKey changes the behavior when the power key is pressed +func switchHandlePowerKey(action string) error { + validActions := map[string]bool{ + "ignore": true, + "poweroff": true, + "reboot": true, + "halt": true, + "kexec": true, + "suspend": true, + "hibernate": true, + "hybrid-sleep": true, + "lock": true, + } + + cfgDir := filepath.Dir(powerBtnCfg()) + if !osutil.IsDirectory(cfgDir) { + if err := os.MkdirAll(cfgDir, 0755); err != nil { + return err + } + } + if !validActions[action] { + return fmt.Errorf("invalid action %q supplied for system.power-key-action option", action) + } + + content := fmt.Sprintf(`[Login] +HandlePowerKey=%s +`, action) + return osutil.AtomicWriteFile(powerBtnCfg(), []byte(content), 0644, 0) +} + +func handlePowerButtonConfiguration(tr Conf) error { + output, err := coreCfg(tr, "system.power-key-action") + if err != nil { + return err + } + if output == "" { + if err := os.Remove(powerBtnCfg()); err != nil && !os.IsNotExist(err) { + return err + } + + } else { + if err := switchHandlePowerKey(output); err != nil { + return err + } + } + return nil +} diff --git a/corecfg/powerbtn_test.go b/corecfg/powerbtn_test.go new file mode 100644 index 00000000..b5d9f321 --- /dev/null +++ b/corecfg/powerbtn_test.go @@ -0,0 +1,79 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/corecfg" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" +) + +type powerbtnSuite struct { + coreCfgSuite + + mockPowerBtnCfg string +} + +var _ = Suite(&powerbtnSuite{}) + +func (s *powerbtnSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "etc"), 0755), IsNil) + + s.mockPowerBtnCfg = filepath.Join(dirs.GlobalRootDir, "/etc/systemd/logind.conf.d/00-snap-core.conf") +} + +func (s *powerbtnSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +func (s *powerbtnSuite) TestConfigurePowerButtonInvalid(c *C) { + err := corecfg.SwitchHandlePowerKey("invalid-action") + c.Check(err, ErrorMatches, `invalid action "invalid-action" supplied for system.power-key-action option`) +} + +func (s *powerbtnSuite) TestConfigurePowerIntegration(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + for _, action := range []string{"ignore", "poweroff", "reboot", "halt", "kexec", "suspend", "hibernate", "hybrid-sleep", "lock"} { + + err := corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + "system.power-key-action": action, + }, + }) + c.Assert(err, IsNil) + + // ensure nothing gets enabled/disabled when an unsupported + // service is set for disable + content, err := ioutil.ReadFile(s.mockPowerBtnCfg) + c.Assert(err, IsNil) + c.Check(string(content), Equals, fmt.Sprintf("[Login]\nHandlePowerKey=%s\n", action)) + } + +} diff --git a/corecfg/proxy.go b/corecfg/proxy.go new file mode 100644 index 00000000..b839361a --- /dev/null +++ b/corecfg/proxy.go @@ -0,0 +1,96 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/overlord/assertstate" +) + +var proxyConfigKeys = map[string]bool{ + "http_proxy": true, + "https_proxy": true, + "ftp_proxy": true, +} + +func etcEnvironment() string { + return filepath.Join(dirs.GlobalRootDir, "/etc/environment") +} + +func updateEtcEnvironmentConfig(path string, config map[string]string) error { + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return nil + } + defer f.Close() + + toWrite, err := updateKeyValueStream(f, proxyConfigKeys, config) + if err != nil { + return err + } + if toWrite != nil { + return ioutil.WriteFile(path, []byte(strings.Join(toWrite, "\n")), 0644) + } + + return nil +} + +func handleProxyConfiguration(tr Conf) error { + config := map[string]string{} + for _, key := range []string{"http", "https", "ftp"} { + output, err := coreCfg(tr, "proxy."+key) + if err != nil { + return err + } + config[key+"_proxy"] = output + } + if err := updateEtcEnvironmentConfig(etcEnvironment(), config); err != nil { + return err + } + + return nil +} + +func validateProxyStore(tr Conf) error { + proxyStore, err := coreCfg(tr, "proxy.store") + if err != nil { + return err + } + + if proxyStore == "" { + return nil + } + + st := tr.State() + st.Lock() + defer st.Unlock() + _, err = assertstate.Store(st, proxyStore) + if asserts.IsNotFound(err) { + return fmt.Errorf("cannot set proxy.store to %q without a matching store assertion", proxyStore) + } + return err +} diff --git a/corecfg/proxy_test.go b/corecfg/proxy_test.go new file mode 100644 index 00000000..8278c9e3 --- /dev/null +++ b/corecfg/proxy_test.go @@ -0,0 +1,144 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/corecfg" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/release" +) + +type proxySuite struct { + coreCfgSuite + + mockEtcEnvironment string + + storeSigning *assertstest.StoreStack +} + +var _ = Suite(&proxySuite{}) + +func (s *proxySuite) SetUpTest(c *C) { + s.coreCfgSuite.SetUpTest(c) + + dirs.SetRootDir(c.MkDir()) + err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/etc/"), 0755) + c.Assert(err, IsNil) + s.mockEtcEnvironment = filepath.Join(dirs.GlobalRootDir, "/etc/environment") + + s.storeSigning = assertstest.NewStoreStack("canonical", nil) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.storeSigning.Trusted, + OtherPredefined: s.storeSigning.Generic, + }) + c.Assert(err, IsNil) + + s.state.Lock() + assertstate.ReplaceDB(s.state, db) + s.state.Unlock() + + err = db.Add(s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) +} + +func (s *proxySuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +func (s *proxySuite) TestConfigureProxy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + for _, proto := range []string{"http", "https", "ftp"} { + // populate with content + err := ioutil.WriteFile(s.mockEtcEnvironment, []byte(` +PATH="/usr/bin" +`), 0644) + c.Assert(err, IsNil) + + err = corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + fmt.Sprintf("proxy.%s", proto): fmt.Sprintf("%s://example.com", proto), + }, + }) + c.Assert(err, IsNil) + + content, err := ioutil.ReadFile(s.mockEtcEnvironment) + c.Assert(err, IsNil) + c.Check(string(content), Equals, fmt.Sprintf(` +PATH="/usr/bin" +%[1]s_proxy=%[1]s://example.com`, proto)) + } +} + +func (s *proxySuite) TestConfigureProxyStore(c *C) { + // set to "" + err := corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + "proxy.store": "", + }, + }) + c.Check(err, IsNil) + + // no assertion + conf := &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "proxy.store": "foo", + }, + } + + err = corecfg.Run(conf) + c.Check(err, ErrorMatches, `cannot set proxy.store to "foo" without a matching store assertion`) + + operatorAcct := assertstest.NewAccount(s.storeSigning, "foo-operator", nil, "") + s.state.Lock() + err = assertstate.Add(s.state, operatorAcct) + s.state.Unlock() + c.Assert(err, IsNil) + + // have a store assertion. + stoAs, err := s.storeSigning.Sign(asserts.StoreType, map[string]interface{}{ + "store": "foo", + "operator-id": operatorAcct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + s.state.Lock() + err = assertstate.Add(s.state, stoAs) + s.state.Unlock() + c.Assert(err, IsNil) + + err = corecfg.Run(conf) + c.Check(err, IsNil) +} diff --git a/corecfg/refresh.go b/corecfg/refresh.go new file mode 100644 index 00000000..4d5185fb --- /dev/null +++ b/corecfg/refresh.go @@ -0,0 +1,51 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg + +import ( + "fmt" + + "github.com/snapcore/snapd/overlord/devicestate" + "github.com/snapcore/snapd/timeutil" +) + +func validateRefreshSchedule(tr Conf) error { + refreshScheduleStr, err := coreCfg(tr, "refresh.schedule") + if err != nil { + return err + } + if refreshScheduleStr == "" { + return nil + } + + if refreshScheduleStr == "managed" { + st := tr.State() + st.Lock() + defer st.Unlock() + + if !devicestate.CanManageRefreshes(st) { + return fmt.Errorf("cannot set schedule to managed") + } + return nil + } + + _, err = timeutil.ParseSchedule(refreshScheduleStr) + return err +} diff --git a/corecfg/refresh_test.go b/corecfg/refresh_test.go new file mode 100644 index 00000000..8f466563 --- /dev/null +++ b/corecfg/refresh_test.go @@ -0,0 +1,50 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/corecfg" +) + +type refreshSuite struct { + coreCfgSuite +} + +var _ = Suite(&refreshSuite{}) + +func (s *refreshSuite) TestConfigureRefreshScheduleHappy(c *C) { + err := corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + "refresh.schedule": "8:00-12:00", + }, + }) + c.Assert(err, IsNil) +} + +func (s *refreshSuite) TestConfigureRefreshScheduleRejected(c *C) { + err := corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + "refresh.schedule": "invalid", + }, + }) + c.Assert(err, ErrorMatches, `cannot parse "invalid": not a valid interval`) +} diff --git a/corecfg/services.go b/corecfg/services.go new file mode 100644 index 00000000..ee835a9c --- /dev/null +++ b/corecfg/services.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg + +import ( + "fmt" + "time" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/systemd" +) + +type sysdLogger struct{} + +func (l *sysdLogger) Notify(status string) { + fmt.Fprintf(Stderr, "sysd: %s\n", status) +} + +// swtichDisableService switches a service in/out of disabled state +// where "true" means disabled and "false" means enabled. +func switchDisableService(service, value string) error { + sysd := systemd.New(dirs.GlobalRootDir, &sysdLogger{}) + serviceName := fmt.Sprintf("%s.service", service) + + switch value { + case "true": + if err := sysd.Disable(serviceName); err != nil { + return err + } + if err := sysd.Mask(serviceName); err != nil { + return err + } + return sysd.Stop(serviceName, 5*time.Minute) + case "false": + if err := sysd.Unmask(serviceName); err != nil { + return err + } + if err := sysd.Enable(serviceName); err != nil { + return err + } + return sysd.Start(serviceName) + default: + return fmt.Errorf("option %q has invalid value %q", serviceName, value) + } +} + +// services that can be disabled +var services = []string{"ssh", "rsyslog"} + +func handleServiceDisableConfiguration(tr Conf) error { + for _, service := range services { + output, err := coreCfg(tr, fmt.Sprintf("service.%s.disable", service)) + if err != nil { + return err + } + if output != "" { + if err := switchDisableService(service, output); err != nil { + return err + } + } + } + + return nil +} diff --git a/corecfg/services_test.go b/corecfg/services_test.go new file mode 100644 index 00000000..44b840a6 --- /dev/null +++ b/corecfg/services_test.go @@ -0,0 +1,141 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/corecfg" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type servicesSuite struct { + coreCfgSuite + testutil.BaseTest +} + +var _ = Suite(&servicesSuite{}) + +func (s *servicesSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + dirs.SetRootDir(c.MkDir()) + c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "etc"), 0755), IsNil) + s.systemctlArgs = nil + s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) +} + +func (s *servicesSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") + s.BaseTest.TearDownTest(c) +} + +func (s *servicesSuite) TestConfigureServiceInvalidValue(c *C) { + err := corecfg.SwitchDisableService("ssh", "xxx") + c.Check(err, ErrorMatches, `option "ssh.service" has invalid value "xxx"`) +} + +func (s *servicesSuite) TestConfigureServiceNotDisabled(c *C) { + err := corecfg.SwitchDisableService("ssh", "false") + c.Assert(err, IsNil) + c.Check(s.systemctlArgs, DeepEquals, [][]string{ + {"--root", dirs.GlobalRootDir, "unmask", "ssh.service"}, + {"--root", dirs.GlobalRootDir, "enable", "ssh.service"}, + {"start", "ssh.service"}, + }) +} + +func (s *servicesSuite) TestConfigureServiceDisabled(c *C) { + err := corecfg.SwitchDisableService("ssh", "true") + c.Assert(err, IsNil) + c.Check(s.systemctlArgs, DeepEquals, [][]string{ + {"--root", dirs.GlobalRootDir, "disable", "ssh.service"}, + {"--root", dirs.GlobalRootDir, "mask", "ssh.service"}, + {"stop", "ssh.service"}, + {"show", "--property=ActiveState", "ssh.service"}, + }) +} + +func (s *servicesSuite) TestConfigureServiceDisabledIntegration(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + for _, srvName := range []string{"ssh", "rsyslog"} { + s.systemctlArgs = nil + + err := corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + fmt.Sprintf("service.%s.disable", srvName): true, + }, + }) + c.Assert(err, IsNil) + srv := fmt.Sprintf("%s.service", srvName) + c.Check(s.systemctlArgs, DeepEquals, [][]string{ + {"--root", dirs.GlobalRootDir, "disable", srv}, + {"--root", dirs.GlobalRootDir, "mask", srv}, + {"stop", srv}, + {"show", "--property=ActiveState", srv}, + }) + } +} + +func (s *servicesSuite) TestConfigureServiceEnableIntegration(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + for _, srvName := range []string{"ssh", "rsyslog"} { + s.systemctlArgs = nil + err := corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + fmt.Sprintf("service.%s.disable", srvName): false, + }, + }) + + c.Assert(err, IsNil) + srv := fmt.Sprintf("%s.service", srvName) + c.Check(s.systemctlArgs, DeepEquals, [][]string{ + {"--root", dirs.GlobalRootDir, "unmask", srv}, + {"--root", dirs.GlobalRootDir, "enable", srv}, + {"start", srv}, + }) + } +} + +func (s *servicesSuite) TestConfigureServiceUnsupportedService(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + err := corecfg.Run(&mockConf{ + conf: map[string]interface{}{ + "service.snapd.disable": true, + }, + }) + c.Assert(err, IsNil) + + // ensure nothing gets enabled/disabled when an unsupported + // service is set for disable + c.Check(s.systemctlArgs, IsNil) +} diff --git a/corecfg/utils.go b/corecfg/utils.go new file mode 100644 index 00000000..c666d139 --- /dev/null +++ b/corecfg/utils.go @@ -0,0 +1,97 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg + +import ( + "bufio" + "fmt" + "io" + "regexp" +) + +// first match is if it is comment, second is key, third value +var rx = regexp.MustCompile(`^[ \t]*(#?)[ \t#]*([a-z_]+)=(.*)$`) + +// updateKeyValueStream updates simple key=value files with comments. +// Example for such formats are: /etc/environment or /boot/uboot/config.txt +// +// An r io.Reader, map of supported config keys and a configuration +// "patch" is taken as input, the r is read line-by-line and any line +// and any required configuration change from the "config" input is +// applied. +// +// If changes need to be written a []string +// that contains the full file is returned. On error an error is returned. +func updateKeyValueStream(r io.Reader, supportedConfigKeys map[string]bool, newConfig map[string]string) (toWrite []string, err error) { + cfgKeys := make([]string, len(newConfig)) + i := 0 + for k := range newConfig { + if !supportedConfigKeys[k] { + return nil, fmt.Errorf("cannot set unsupported configuration value %q", k) + } + cfgKeys[i] = k + i++ + } + + // now go over the content + found := map[string]bool{} + needsWrite := false + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + matches := rx.FindStringSubmatch(line) + if len(matches) > 0 && supportedConfigKeys[matches[2]] { + wasComment := (matches[1] == "#") + key := matches[2] + oldValue := matches[3] + found[key] = true + if newConfig[key] != "" { + if wasComment || oldValue != newConfig[key] { + line = fmt.Sprintf("%s=%s", key, newConfig[key]) + needsWrite = true + } + } else { + if !wasComment { + line = fmt.Sprintf("#%s=%s", key, oldValue) + needsWrite = true + } + } + } + toWrite = append(toWrite, line) + } + if err := scanner.Err(); err != nil { + return nil, err + } + + // write anything that is missing + for key := range newConfig { + if !found[key] && newConfig[key] != "" { + needsWrite = true + toWrite = append(toWrite, fmt.Sprintf("%s=%s", key, newConfig[key])) + } + } + + if needsWrite { + return toWrite, nil + } + + return nil, nil +} diff --git a/corecfg/utils_test.go b/corecfg/utils_test.go new file mode 100644 index 00000000..bb502eee --- /dev/null +++ b/corecfg/utils_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package corecfg_test + +import ( + "bytes" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/corecfg" +) + +type utilsSuite struct{} + +var _ = Suite(&utilsSuite{}) + +func (s *utilsSuite) TestUpdateKeyValueStreamNoNewConfig(c *C) { + in := bytes.NewBufferString("foo=bar") + newConfig := map[string]string{} + supportedConfigKeys := map[string]bool{} + + toWrite, err := corecfg.UpdateKeyValueStream(in, supportedConfigKeys, newConfig) + c.Check(err, IsNil) + c.Check(toWrite, IsNil) +} + +func (s *utilsSuite) TestUpdateKeyValueStreamConfigNotInAllConfig(c *C) { + in := bytes.NewBufferString("") + newConfig := map[string]string{"unsupported-options": "cannot be set"} + supportedConfigKeys := map[string]bool{ + "foo": true, + } + + _, err := corecfg.UpdateKeyValueStream(in, supportedConfigKeys, newConfig) + c.Check(err, ErrorMatches, `cannot set unsupported configuration value "unsupported-options"`) +} + +func (s *utilsSuite) TestUpdateKeyValueStreamOneChange(c *C) { + in := bytes.NewBufferString("foo=bar") + newConfig := map[string]string{"foo": "baz"} + supportedConfigKeys := map[string]bool{ + "foo": true, + } + + toWrite, err := corecfg.UpdateKeyValueStream(in, supportedConfigKeys, newConfig) + c.Check(err, IsNil) + c.Check(toWrite, DeepEquals, []string{"foo=baz"}) +} diff --git a/daemon/api.go b/daemon/api.go new file mode 100644 index 00000000..ddb76484 --- /dev/null +++ b/daemon/api.go @@ -0,0 +1,2702 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "net/http" + "os" + "os/user" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/snapasserts" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/jsonutil" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/devicestate" + "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" + "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/servicestate" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/systemd" +) + +var api = []*Command{ + rootCmd, + sysInfoCmd, + loginCmd, + logoutCmd, + appIconCmd, + findCmd, + snapsCmd, + snapCmd, + snapConfCmd, + interfacesCmd, + assertsCmd, + assertsFindManyCmd, + stateChangeCmd, + stateChangesCmd, + createUserCmd, + buyCmd, + readyToBuyCmd, + snapctlCmd, + usersCmd, + sectionsCmd, + aliasesCmd, + appsCmd, + logsCmd, + debugCmd, +} + +var ( + rootCmd = &Command{ + Path: "/", + GuestOK: true, + GET: tbd, + } + + sysInfoCmd = &Command{ + Path: "/v2/system-info", + GuestOK: true, + GET: sysInfo, + } + + loginCmd = &Command{ + Path: "/v2/login", + POST: loginUser, + PolkitOK: "io.snapcraft.snapd.login", + } + + logoutCmd = &Command{ + Path: "/v2/logout", + POST: logoutUser, + UserOK: true, + } + + appIconCmd = &Command{ + Path: "/v2/icons/{name}/icon", + UserOK: true, + GET: appIconGet, + } + + findCmd = &Command{ + Path: "/v2/find", + UserOK: true, + GET: searchStore, + } + + snapsCmd = &Command{ + Path: "/v2/snaps", + UserOK: true, + PolkitOK: "io.snapcraft.snapd.manage", + GET: getSnapsInfo, + POST: postSnaps, + } + + snapCmd = &Command{ + Path: "/v2/snaps/{name}", + UserOK: true, + PolkitOK: "io.snapcraft.snapd.manage", + GET: getSnapInfo, + POST: postSnap, + } + + appsCmd = &Command{ + Path: "/v2/apps", + UserOK: true, + GET: getAppsInfo, + POST: postApps, + } + + logsCmd = &Command{ + Path: "/v2/logs", + PolkitOK: "io.snapcraft.snapd.manage", + GET: getLogs, + } + + snapConfCmd = &Command{ + Path: "/v2/snaps/{name}/conf", + GET: getSnapConf, + PUT: setSnapConf, + } + + interfacesCmd = &Command{ + Path: "/v2/interfaces", + UserOK: true, + GET: interfacesConnectionsMultiplexer, + POST: changeInterfaces, + } + + // TODO: allow to post assertions for UserOK? they are verified anyway + assertsCmd = &Command{ + Path: "/v2/assertions", + UserOK: true, + GET: getAssertTypeNames, + POST: doAssert, + } + + assertsFindManyCmd = &Command{ + Path: "/v2/assertions/{assertType}", + UserOK: true, + GET: assertsFindMany, + } + + stateChangeCmd = &Command{ + Path: "/v2/changes/{id}", + UserOK: true, + PolkitOK: "io.snapcraft.snapd.manage", + GET: getChange, + POST: abortChange, + } + + stateChangesCmd = &Command{ + Path: "/v2/changes", + UserOK: true, + GET: getChanges, + } + + debugCmd = &Command{ + Path: "/v2/debug", + POST: postDebug, + } + + createUserCmd = &Command{ + Path: "/v2/create-user", + UserOK: false, + POST: postCreateUser, + } + + buyCmd = &Command{ + Path: "/v2/buy", + UserOK: false, + POST: postBuy, + } + + readyToBuyCmd = &Command{ + Path: "/v2/buy/ready", + UserOK: false, + GET: readyToBuy, + } + + snapctlCmd = &Command{ + Path: "/v2/snapctl", + SnapOK: true, + POST: runSnapctl, + } + + usersCmd = &Command{ + Path: "/v2/users", + UserOK: false, + GET: getUsers, + } + + sectionsCmd = &Command{ + Path: "/v2/sections", + UserOK: true, + GET: getSections, + } + + aliasesCmd = &Command{ + Path: "/v2/aliases", + UserOK: true, + GET: getAliases, + POST: changeAliases, + } +) + +func tbd(c *Command, r *http.Request, user *auth.UserState) Response { + return SyncResponse([]string{"TBD"}, nil) +} + +func formatRefreshTime(t time.Time) string { + if t.IsZero() { + return "" + } + return fmt.Sprintf("%s", t.Truncate(time.Minute).Format(time.RFC3339)) +} + +func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response { + st := c.d.overlord.State() + snapMgr := c.d.overlord.SnapManager() + st.Lock() + nextRefresh := snapMgr.NextRefresh() + lastRefresh, _ := snapMgr.LastRefresh() + refreshScheduleStr, err := snapMgr.RefreshSchedule() + if err != nil { + return InternalError("cannot get refresh schedule: %s", err) + } + users, err := auth.Users(st) + st.Unlock() + if err != nil && err != state.ErrNoState { + return InternalError("cannot get user auth data: %s", err) + } + + m := map[string]interface{}{ + "series": release.Series, + "version": c.d.Version, + "os-release": release.ReleaseInfo, + "on-classic": release.OnClassic, + "managed": len(users) > 0, + "kernel-version": release.KernelVersion(), + "locations": map[string]interface{}{ + "snap-mount-dir": dirs.SnapMountDir, + "snap-bin-dir": dirs.SnapBinariesDir, + }, + "refresh": client.RefreshInfo{ + Schedule: refreshScheduleStr, + Last: formatRefreshTime(lastRefresh), + Next: formatRefreshTime(nextRefresh), + }, + } + // NOTE: Right now we don't have a good way to differentiate if we + // only have partial confinement (ala AppArmor disabled and Seccomp + // enabled) or no confinement at all. Once we have a better system + // in place how we can dynamically retrieve these information from + // snapd we will use this here. + if release.ReleaseInfo.ForceDevMode() { + m["confinement"] = "partial" + } else { + m["confinement"] = "strict" + } + + return SyncResponse(m, nil) +} + +// userResponseData contains the data releated to user creation/login/query +type userResponseData struct { + ID int `json:"id,omitempty"` + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + SSHKeys []string `json:"ssh-keys,omitempty"` + + Macaroon string `json:"macaroon,omitempty"` + Discharges []string `json:"discharges,omitempty"` +} + +var isEmailish = regexp.MustCompile(`.@.*\..`).MatchString + +func loginUser(c *Command, r *http.Request, user *auth.UserState) Response { + var loginData struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + Otp string `json:"otp"` + } + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&loginData); err != nil { + return BadRequest("cannot decode login data from request body: %v", err) + } + + if loginData.Email == "" && isEmailish(loginData.Username) { + // for backwards compatibility, if no email is provided assume username is the email + loginData.Email = loginData.Username + loginData.Username = "" + } + + if loginData.Email == "" && user != nil && user.Email != "" { + loginData.Email = user.Email + } + + // the "username" needs to look a lot like an email address + if !isEmailish(loginData.Email) { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: "please use a valid email address.", + Kind: errorKindInvalidAuthData, + Value: map[string][]string{"email": {"invalid"}}, + }, + Status: 400, + }, nil) + } + + macaroon, discharge, err := store.LoginUser(loginData.Email, loginData.Password, loginData.Otp) + switch err { + case store.ErrAuthenticationNeeds2fa: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Kind: errorKindTwoFactorRequired, + Message: err.Error(), + }, + Status: 401, + }, nil) + case store.Err2faFailed: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Kind: errorKindTwoFactorFailed, + Message: err.Error(), + }, + Status: 401, + }, nil) + default: + switch err := err.(type) { + case store.InvalidAuthDataError: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindInvalidAuthData, + Value: err, + }, + Status: 400, + }, nil) + case store.PasswordPolicyError: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindPasswordPolicy, + Value: err, + }, + Status: 401, + }, nil) + } + return Unauthorized(err.Error()) + case nil: + // continue + } + overlord := c.d.overlord + state := overlord.State() + state.Lock() + if user != nil { + // local user logged-in, set its store macaroons + user.StoreMacaroon = macaroon + user.StoreDischarges = []string{discharge} + err = auth.UpdateUser(state, user) + } else { + user, err = auth.NewUser(state, loginData.Username, loginData.Email, macaroon, []string{discharge}) + } + state.Unlock() + if err != nil { + return InternalError("cannot persist authentication details: %v", err) + } + + result := userResponseData{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Macaroon: user.Macaroon, + Discharges: user.Discharges, + } + return SyncResponse(result, nil) +} + +func logoutUser(c *Command, r *http.Request, user *auth.UserState) Response { + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + if user == nil { + return BadRequest("not logged in") + } + err := auth.RemoveUser(state, user.ID) + if err != nil { + return InternalError(err.Error()) + } + + return SyncResponse(nil, nil) +} + +// UserFromRequest extracts user information from request and return the respective user in state, if valid +// It requires the state to be locked +func UserFromRequest(st *state.State, req *http.Request) (*auth.UserState, error) { + // extract macaroons data from request + header := req.Header.Get("Authorization") + if header == "" { + return nil, auth.ErrInvalidAuth + } + + authorizationData := strings.SplitN(header, " ", 2) + if len(authorizationData) != 2 || authorizationData[0] != "Macaroon" { + return nil, fmt.Errorf("authorization header misses Macaroon prefix") + } + + var macaroon string + var discharges []string + for _, field := range splitQS(authorizationData[1]) { + if strings.HasPrefix(field, `root="`) { + macaroon = strings.TrimSuffix(field[6:], `"`) + } + if strings.HasPrefix(field, `discharge="`) { + discharges = append(discharges, strings.TrimSuffix(field[11:], `"`)) + } + } + + if macaroon == "" { + return nil, fmt.Errorf("invalid authorization header") + } + + user, err := auth.CheckMacaroon(st, macaroon, discharges) + return user, err +} + +var muxVars = mux.Vars + +func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response { + vars := muxVars(r) + name := vars["name"] + + about, err := localSnapInfo(c.d.overlord.State(), name) + if err != nil { + if err == errNoSnap { + return SnapNotFound(name, err) + } + + return InternalError("%v", err) + } + + route := c.d.router.Get(c.Path) + if route == nil { + return InternalError("cannot find route for %q snap", name) + } + + url, err := route.URL("name", name) + if err != nil { + return InternalError("cannot build URL for %q snap: %v", name, err) + } + + result := webify(mapLocal(about), url.String()) + + return SyncResponse(result, nil) +} + +func webify(result *client.Snap, resource string) *client.Snap { + if result.Icon == "" || strings.HasPrefix(result.Icon, "http") { + return result + } + result.Icon = "" + + route := appIconCmd.d.router.Get(appIconCmd.Path) + if route != nil { + url, err := route.URL("name", result.Name) + if err == nil { + result.Icon = url.String() + } + } + + return result +} + +func getStore(c *Command) snapstate.StoreService { + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + return snapstate.Store(st) +} + +func getSections(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(snapCmd.Path) + if route == nil { + return InternalError("cannot find route for snaps") + } + + theStore := getStore(c) + + sections, err := theStore.Sections(user) + switch err { + case nil: + // pass + case store.ErrEmptyQuery, store.ErrBadQuery: + return BadRequest("%v", err) + case store.ErrUnauthenticated, store.ErrInvalidCredentials: + return Unauthorized("%v", err) + default: + return InternalError("%v", err) + } + + return SyncResponse(sections, &Meta{}) +} + +func searchStore(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(snapCmd.Path) + if route == nil { + return InternalError("cannot find route for snaps") + } + query := r.URL.Query() + q := query.Get("q") + section := query.Get("section") + name := query.Get("name") + private := false + prefix := false + + if name != "" { + if q != "" { + return BadRequest("cannot use 'q' and 'name' together") + } + + if name[len(name)-1] != '*' { + return findOne(c, r, user, name) + } + + prefix = true + q = name[:len(name)-1] + } + + if sel := query.Get("select"); sel != "" { + switch sel { + case "refresh": + if prefix { + return BadRequest("cannot use 'name' with 'select=refresh'") + } + if q != "" { + return BadRequest("cannot use 'q' with 'select=refresh'") + } + return storeUpdates(c, r, user) + case "private": + private = true + } + } + + theStore := getStore(c) + found, err := theStore.Find(&store.Search{ + Query: q, + Section: section, + Private: private, + Prefix: prefix, + }, user) + switch err { + case nil: + // pass + case store.ErrEmptyQuery, store.ErrBadQuery: + return BadRequest("%v", err) + case store.ErrUnauthenticated, store.ErrInvalidCredentials: + return Unauthorized(err.Error()) + default: + return InternalError("%v", err) + } + + meta := &Meta{ + SuggestedCurrency: theStore.SuggestedCurrency(), + Sources: []string{"store"}, + } + + return sendStorePackages(route, meta, found) +} + +func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response { + if err := snap.ValidateName(name); err != nil { + return BadRequest(err.Error()) + } + + theStore := getStore(c) + spec := store.SnapSpec{ + Name: name, + AnyChannel: true, + } + snapInfo, err := theStore.SnapInfo(spec, user) + switch err { + case nil: + // pass + case store.ErrInvalidCredentials: + return Unauthorized("%v", err) + case store.ErrSnapNotFound: + return SnapNotFound(name, err) + default: + return InternalError("%v", err) + } + + meta := &Meta{ + SuggestedCurrency: theStore.SuggestedCurrency(), + Sources: []string{"store"}, + } + + results := make([]*json.RawMessage, 1) + data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String())) + if err != nil { + return InternalError(err.Error()) + } + results[0] = (*json.RawMessage)(&data) + return SyncResponse(results, meta) +} + +func shouldSearchStore(r *http.Request) bool { + // we should jump to the old behaviour iff q is given, or if + // sources is given and either empty or contains the word + // 'store'. Otherwise, local results only. + + query := r.URL.Query() + + if _, ok := query["q"]; ok { + logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL) + return true + } + + if src, ok := query["sources"]; ok { + logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL) + if len(src) == 0 || strings.Contains(src[0], "store") { + return true + } + } + + return false +} + +func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(snapCmd.Path) + if route == nil { + return InternalError("cannot find route for snaps") + } + + state := c.d.overlord.State() + state.Lock() + updates, err := snapstateRefreshCandidates(state, user) + state.Unlock() + if err != nil { + return InternalError("cannot list updates: %v", err) + } + + return sendStorePackages(route, nil, updates) +} + +func sendStorePackages(route *mux.Route, meta *Meta, found []*snap.Info) Response { + results := make([]*json.RawMessage, 0, len(found)) + for _, x := range found { + url, err := route.URL("name", x.Name()) + if err != nil { + logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.Name(), x.Revision, err) + continue + } + + data, err := json.Marshal(webify(mapRemote(x), url.String())) + if err != nil { + return InternalError("%v", err) + } + raw := json.RawMessage(data) + results = append(results, &raw) + } + + return SyncResponse(results, meta) +} + +// plural! +func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response { + + if shouldSearchStore(r) { + logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL) + return searchStore(c, r, user) + } + + route := c.d.router.Get(snapCmd.Path) + if route == nil { + return InternalError("cannot find route for snaps") + } + + query := r.URL.Query() + var all bool + sel := query.Get("select") + switch sel { + case "all": + all = true + case "enabled", "": + all = false + default: + return BadRequest("invalid select parameter: %q", sel) + } + var wanted map[string]bool + if ns := query.Get("snaps"); len(ns) > 0 { + nsl := splitQS(ns) + wanted = make(map[string]bool, len(nsl)) + for _, name := range nsl { + wanted[name] = true + } + } + + found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted) + if err != nil { + return InternalError("cannot list local snaps! %v", err) + } + + results := make([]*json.RawMessage, len(found)) + + for i, x := range found { + name := x.info.Name() + rev := x.info.Revision + + url, err := route.URL("name", name) + if err != nil { + logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err) + continue + } + + data, err := json.Marshal(webify(mapLocal(x), url.String())) + if err != nil { + return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err) + } + raw := json.RawMessage(data) + results[i] = &raw + } + + return SyncResponse(results, &Meta{Sources: []string{"local"}}) +} + +func resultHasType(r map[string]interface{}, allowedTypes []string) bool { + for _, t := range allowedTypes { + if r["type"] == t { + return true + } + } + return false +} + +// licenseData holds details about the snap license, and may be +// marshaled back as an error when the license agreement is pending, +// and is expected as input to accept (or not) that license +// agreement. As such, its field names are part of the API. +type licenseData struct { + Intro string `json:"intro"` + License string `json:"license"` + Agreed bool `json:"agreed"` +} + +func (*licenseData) Error() string { + return "license agreement required" +} + +type snapInstruction struct { + progress.NullMeter + Action string `json:"action"` + Channel string `json:"channel"` + Revision snap.Revision `json:"revision"` + DevMode bool `json:"devmode"` + JailMode bool `json:"jailmode"` + Classic bool `json:"classic"` + IgnoreValidation bool `json:"ignore-validation"` + Unaliased bool `json:"unaliased"` + // dropping support temporarely until flag confusion is sorted, + // this isn't supported by client atm anyway + LeaveOld bool `json:"temp-dropped-leave-old"` + License *licenseData `json:"license"` + Snaps []string `json:"snaps"` + + // The fields below should not be unmarshalled into. Do not export them. + userID int +} + +func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) { + return modeFlags(inst.DevMode, inst.JailMode, inst.Classic) +} + +func (inst *snapInstruction) installFlags() (snapstate.Flags, error) { + flags, err := inst.modeFlags() + if err != nil { + return snapstate.Flags{}, err + } + if inst.Unaliased { + flags.Unaliased = true + } + return flags, nil +} + +var ( + snapstateInstall = snapstate.Install + snapstateInstallPath = snapstate.InstallPath + snapstateRefreshCandidates = snapstate.RefreshCandidates + snapstateTryPath = snapstate.TryPath + snapstateUpdate = snapstate.Update + snapstateUpdateMany = snapstate.UpdateMany + snapstateInstallMany = snapstate.InstallMany + snapstateRemoveMany = snapstate.RemoveMany + snapstateRevert = snapstate.Revert + snapstateRevertToRevision = snapstate.RevertToRevision + snapstateSwitch = snapstate.Switch + + assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations +) + +func ensureStateSoonImpl(st *state.State) { + st.EnsureBefore(0) +} + +var ensureStateSoon = ensureStateSoonImpl + +var errNothingToInstall = errors.New("nothing to install") + +var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together") +var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together") +var errNoJailMode = errors.New("this system cannot honour the jailmode flag") + +func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) { + flags := snapstate.Flags{} + devModeOS := release.ReleaseInfo.ForceDevMode() + switch { + case jailMode && devModeOS: + return flags, errNoJailMode + case jailMode && devMode: + return flags, errDevJailModeConflict + case devMode && classic: + return flags, errClassicDevmodeConflict + } + // NOTE: jailmode and classic are allowed together. In that setting, + // jailmode overrides classic and the app gets regular (non-classic) + // confinement. + flags.JailMode = jailMode + flags.Classic = classic + flags.DevMode = devMode + return flags, nil +} + +func snapUpdateMany(inst *snapInstruction, st *state.State) (msg string, updated []string, tasksets []*state.TaskSet, err error) { + // we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly + if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { + return "", nil, nil, err + } + + updated, tasksets, err = snapstateUpdateMany(st, inst.Snaps, inst.userID) + if err != nil { + return "", nil, nil, err + } + + switch len(updated) { + case 0: + if len(inst.Snaps) != 0 { + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps)) + } else { + msg = fmt.Sprintf(i18n.G("Refresh all snaps: no updates")) + } + case 1: + msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) + default: + quoted := strutil.Quoted(updated) + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted) + } + + return msg, updated, tasksets, nil +} + +func verifySnapInstructions(inst *snapInstruction) error { + switch inst.Action { + case "install": + for _, snapName := range inst.Snaps { + // FIXME: alternatively we could simply mutate *inst + // and s/ubuntu-core/core/ ? + if snapName == "ubuntu-core" { + return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`) + } + } + } + + return nil +} + +func snapInstallMany(inst *snapInstruction, st *state.State) (msg string, installed []string, tasksets []*state.TaskSet, err error) { + installed, tasksets, err = snapstateInstallMany(st, inst.Snaps, inst.userID) + if err != nil { + return "", nil, nil, err + } + + switch len(inst.Snaps) { + case 0: + return "", nil, nil, fmt.Errorf("cannot install zero snaps") + case 1: + msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0]) + default: + quoted := strutil.Quoted(inst.Snaps) + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted) + } + + return msg, installed, tasksets, nil +} + +func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + flags, err := inst.installFlags() + if err != nil { + return "", nil, err + } + + logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision) + + tset, err := snapstateInstall(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0]) + if inst.Channel != "stable" && inst.Channel != "" { + msg = fmt.Sprintf(i18n.G("Install %q snap from %q channel"), inst.Snaps[0], inst.Channel) + } + return msg, []*state.TaskSet{tset}, nil +} + +func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + // TODO: bail if revision is given (and != current?), *or* behave as with install --revision? + flags, err := inst.modeFlags() + if err != nil { + return "", nil, err + } + if inst.IgnoreValidation { + flags.IgnoreValidation = true + } + + // we need refreshed snap-declarations to enforce refresh-control as best as we can + if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { + return "", nil, err + } + + ts, err := snapstateUpdate(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0]) + if inst.Channel != "stable" && inst.Channel != "" { + msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel) + } + + return msg, []*state.TaskSet{ts}, nil +} + +func snapRemoveMany(inst *snapInstruction, st *state.State) (msg string, removed []string, tasksets []*state.TaskSet, err error) { + removed, tasksets, err = snapstateRemoveMany(st, inst.Snaps) + if err != nil { + return "", nil, nil, err + } + + switch len(inst.Snaps) { + case 0: + return "", nil, nil, fmt.Errorf("cannot remove zero snaps") + case 1: + msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0]) + default: + quoted := strutil.Quoted(inst.Snaps) + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted) + } + + return msg, removed, tasksets, nil +} + +func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0]) + return msg, []*state.TaskSet{ts}, nil +} + +func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + var ts *state.TaskSet + + flags, err := inst.modeFlags() + if err != nil { + return "", nil, err + } + + if inst.Revision.Unset() { + ts, err = snapstateRevert(st, inst.Snaps[0], flags) + } else { + ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags) + } + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0]) + return msg, []*state.TaskSet{ts}, nil +} + +func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + if !inst.Revision.Unset() { + return "", nil, errors.New("enable takes no revision") + } + ts, err := snapstate.Enable(st, inst.Snaps[0]) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0]) + return msg, []*state.TaskSet{ts}, nil +} + +func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + if !inst.Revision.Unset() { + return "", nil, errors.New("disable takes no revision") + } + ts, err := snapstate.Disable(st, inst.Snaps[0]) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0]) + return msg, []*state.TaskSet{ts}, nil +} + +func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + if !inst.Revision.Unset() { + return "", nil, errors.New("switch takes no revision") + } + ts, err := snapstate.Switch(st, inst.Snaps[0], inst.Channel) + if err != nil { + return "", nil, err + } + + msg := fmt.Sprintf(i18n.G("Switch %q snap to %s"), inst.Snaps[0], inst.Channel) + return msg, []*state.TaskSet{ts}, nil +} + +type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) + +var snapInstructionDispTable = map[string]snapActionFunc{ + "install": snapInstall, + "refresh": snapUpdate, + "remove": snapRemove, + "revert": snapRevert, + "enable": snapEnable, + "disable": snapDisable, + "switch": snapSwitch, +} + +func (inst *snapInstruction) dispatch() snapActionFunc { + if len(inst.Snaps) != 1 { + logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps)) + } + return snapInstructionDispTable[inst.Action] +} + +func (inst *snapInstruction) errToResponse(err error) Response { + var kind errorKind + + switch err { + case store.ErrSnapNotFound: + return SnapNotFound(inst.Snaps[0], err) + case store.ErrNoUpdateAvailable: + kind = errorKindSnapNoUpdateAvailable + case store.ErrLocalSnap: + kind = errorKindSnapLocal + default: + switch err := err.(type) { + case *snap.AlreadyInstalledError: + kind = errorKindSnapAlreadyInstalled + case *snap.NotInstalledError: + kind = errorKindSnapNotInstalled + case *snapstate.SnapNeedsDevModeError: + kind = errorKindSnapNeedsDevMode + case *snapstate.SnapNeedsClassicError: + kind = errorKindSnapNeedsClassic + case *snapstate.SnapNeedsClassicSystemError: + kind = errorKindSnapNeedsClassicSystem + default: + return BadRequest("cannot %s %q: %v", inst.Action, inst.Snaps[0], err) + } + } + + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{Message: err.Error(), Kind: kind}, + Status: 400, + }, nil) +} + +func postSnap(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(stateChangeCmd.Path) + if route == nil { + return InternalError("cannot find route for change") + } + + decoder := json.NewDecoder(r.Body) + var inst snapInstruction + if err := decoder.Decode(&inst); err != nil { + return BadRequest("cannot decode request body into snap instruction: %v", err) + } + + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + if user != nil { + inst.userID = user.ID + } + + vars := muxVars(r) + inst.Snaps = []string{vars["name"]} + + if err := verifySnapInstructions(&inst); err != nil { + return BadRequest("%s", err) + } + + impl := inst.dispatch() + if impl == nil { + return BadRequest("unknown action %s", inst.Action) + } + + msg, tsets, err := impl(&inst, state) + if err != nil { + return inst.errToResponse(err) + } + + chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps) + + ensureStateSoon(state) + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} + +func newChange(st *state.State, kind, summary string, tsets []*state.TaskSet, snapNames []string) *state.Change { + chg := st.NewChange(kind, summary) + for _, ts := range tsets { + chg.AddAll(ts) + } + if snapNames != nil { + chg.Set("snap-names", snapNames) + } + return chg +} + +const maxReadBuflen = 1024 * 1024 + +func trySnap(c *Command, r *http.Request, user *auth.UserState, trydir string, flags snapstate.Flags) Response { + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + if !filepath.IsAbs(trydir) { + return BadRequest("cannot try %q: need an absolute path", trydir) + } + if !osutil.IsDirectory(trydir) { + return BadRequest("cannot try %q: not a snap directory", trydir) + } + + // the developer asked us to do this with a trusted snap dir + info, err := unsafeReadSnapInfo(trydir) + if _, ok := err.(snap.NotSnapError); ok { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindNotSnap, + }, + Status: 400, + }, nil) + } + if err != nil { + return BadRequest("cannot read snap info for %s: %s", trydir, err) + } + + tset, err := snapstateTryPath(st, info.Name(), trydir, flags) + if err != nil { + return BadRequest("cannot try %s: %s", trydir, err) + } + + msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.Name(), trydir) + chg := newChange(st, "try-snap", msg, []*state.TaskSet{tset}, []string{info.Name()}) + chg.Set("api-data", map[string]string{"snap-name": info.Name()}) + + ensureStateSoon(st) + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} + +func isTrue(form *multipart.Form, key string) bool { + value := form.Value[key] + if len(value) == 0 { + return false + } + b, err := strconv.ParseBool(value[0]) + if err != nil { + return false + } + + return b +} + +func snapsOp(c *Command, r *http.Request, user *auth.UserState) Response { + route := c.d.router.Get(stateChangeCmd.Path) + if route == nil { + return InternalError("cannot find route for change") + } + + decoder := json.NewDecoder(r.Body) + var inst snapInstruction + if err := decoder.Decode(&inst); err != nil { + return BadRequest("cannot decode request body into snap instruction: %v", err) + } + + if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode { + return BadRequest("unsupported option provided for multi-snap operation") + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + if user != nil { + inst.userID = user.ID + } + + var msg string + var affected []string + var tsets []*state.TaskSet + var err error + switch inst.Action { + case "refresh": + msg, affected, tsets, err = snapUpdateMany(&inst, st) + case "install": + msg, affected, tsets, err = snapInstallMany(&inst, st) + case "remove": + msg, affected, tsets, err = snapRemoveMany(&inst, st) + default: + return BadRequest("unsupported multi-snap operation %q", inst.Action) + } + if err != nil { + return InternalError("cannot %s %q: %v", inst.Action, inst.Snaps, err) + } + + var chg *state.Change + if len(tsets) == 0 { + chg = st.NewChange(inst.Action+"-snap", msg) + chg.SetStatus(state.DoneStatus) + } else { + chg = newChange(st, inst.Action+"-snap", msg, tsets, affected) + ensureStateSoon(st) + } + chg.Set("api-data", map[string]interface{}{"snap-names": affected}) + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} + +func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { + contentType := r.Header.Get("Content-Type") + + if contentType == "application/json" { + return snapsOp(c, r, user) + } + + if !strings.HasPrefix(contentType, "multipart/") { + return BadRequest("unknown content type: %s", contentType) + } + + route := c.d.router.Get(stateChangeCmd.Path) + if route == nil { + return InternalError("cannot find route for change") + } + + // POSTs to sideload snaps must be a multipart/form-data file upload. + _, params, err := mime.ParseMediaType(contentType) + if err != nil { + return BadRequest("cannot parse POST body: %v", err) + } + + form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen) + if err != nil { + return BadRequest("cannot read POST form: %v", err) + } + + dangerousOK := isTrue(form, "dangerous") + flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic")) + if err != nil { + return BadRequest(err.Error()) + } + + if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" { + if len(form.Value["snap-path"]) == 0 { + return BadRequest("need 'snap-path' value in form") + } + return trySnap(c, r, user, form.Value["snap-path"][0], flags) + } + flags.RemoveSnapPath = true + + // find the file for the "snap" form field + var snapBody multipart.File + var origPath string +out: + for name, fheaders := range form.File { + if name != "snap" { + continue + } + for _, fheader := range fheaders { + snapBody, err = fheader.Open() + origPath = fheader.Filename + if err != nil { + return BadRequest(`cannot open uploaded "snap" file: %v`, err) + } + defer snapBody.Close() + + break out + } + } + defer form.RemoveAll() + + if snapBody == nil { + return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) + } + + // we are in charge of the tempfile life cycle until we hand it off to the change + changeTriggered := false + // if you change this prefix, look for it in the tests + tmpf, err := ioutil.TempFile("", "snapd-sideload-pkg-") + if err != nil { + return InternalError("cannot create temporary file: %v", err) + } + + tempPath := tmpf.Name() + + defer func() { + if !changeTriggered { + os.Remove(tempPath) + } + }() + + if _, err := io.Copy(tmpf, snapBody); err != nil { + return InternalError("cannot copy request into temporary file: %v", err) + } + tmpf.Sync() + + if len(form.Value["snap-path"]) > 0 { + origPath = form.Value["snap-path"][0] + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + var snapName string + var sideInfo *snap.SideInfo + + if !dangerousOK { + si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st)) + switch { + case err == nil: + snapName = si.RealName + sideInfo = si + case asserts.IsNotFound(err): + // with devmode we try to find assertions but it's ok + // if they are not there (implies --dangerous) + if !isTrue(form, "devmode") { + msg := "cannot find signatures with metadata for snap" + if origPath != "" { + msg = fmt.Sprintf("%s %q", msg, origPath) + } + return BadRequest(msg) + } + // TODO: set a warning if devmode + default: + return BadRequest(err.Error()) + } + } + + if snapName == "" { + // potentially dangerous but dangerous or devmode params were set + info, err := unsafeReadSnapInfo(tempPath) + if err != nil { + return BadRequest("cannot read snap file: %v", err) + } + snapName = info.Name() + sideInfo = &snap.SideInfo{RealName: snapName} + } + + msg := fmt.Sprintf(i18n.G("Install %q snap from file"), snapName) + if origPath != "" { + msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), snapName, origPath) + } + + tset, err := snapstateInstallPath(st, sideInfo, tempPath, "", flags) + if err != nil { + return InternalError("cannot install snap file: %v", err) + } + + chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{snapName}) + chg.Set("api-data", map[string]string{"snap-name": snapName}) + + ensureStateSoon(st) + + // only when the unlock succeeds (as opposed to panicing) is the handoff done + // but this is good enough + changeTriggered = true + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} + +func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) { + // Condider using DeriveSideInfo before falling back to this! + snapf, err := snap.Open(snapPath) + if err != nil { + return nil, err + } + return snap.ReadInfoFromSnapFile(snapf, nil) +} + +var unsafeReadSnapInfo = unsafeReadSnapInfoImpl + +func iconGet(st *state.State, name string) Response { + about, err := localSnapInfo(st, name) + if err != nil { + if err == errNoSnap { + return SnapNotFound(name, err) + } + return InternalError("%v", err) + } + + path := filepath.Clean(snapIcon(about.info)) + if !strings.HasPrefix(path, dirs.SnapMountDir) { + // XXX: how could this happen? + return BadRequest("requested icon is not in snap path") + } + + return FileResponse(path) +} + +func appIconGet(c *Command, r *http.Request, user *auth.UserState) Response { + vars := muxVars(r) + name := vars["name"] + + return iconGet(c.d.overlord.State(), name) +} + +func splitQS(qs string) []string { + qsl := strings.Split(qs, ",") + split := make([]string, 0, len(qsl)) + for _, elem := range qsl { + elem = strings.TrimSpace(elem) + if len(elem) > 0 { + split = append(split, elem) + } + } + + return split +} + +func getSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { + vars := muxVars(r) + snapName := vars["name"] + + keys := splitQS(r.URL.Query().Get("keys")) + + s := c.d.overlord.State() + s.Lock() + tr := config.NewTransaction(s) + s.Unlock() + + currentConfValues := make(map[string]interface{}) + // Special case - return root document + if len(keys) == 0 { + keys = []string{""} + } + for _, key := range keys { + var value interface{} + if err := tr.Get(snapName, key, &value); err != nil { + if config.IsNoOption(err) { + if key == "" { + // no configuration - return empty document + currentConfValues = make(map[string]interface{}) + break + } + return BadRequest("%v", err) + } else { + return InternalError("%v", err) + } + } + if key == "" { + if len(keys) > 1 { + return BadRequest("keys contains zero-length string") + } + return SyncResponse(value, nil) + } + + currentConfValues[key] = value + } + + return SyncResponse(currentConfValues, nil) +} + +func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { + vars := muxVars(r) + snapName := vars["name"] + + var patchValues map[string]interface{} + if err := jsonutil.DecodeWithNumber(r.Body, &patchValues); err != nil { + return BadRequest("cannot decode request body into patch values: %v", err) + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + taskset, err := configstate.ConfigureInstalled(st, snapName, patchValues, 0) + if err != nil { + if _, ok := err.(*snap.NotInstalledError); ok { + return SnapNotFound(snapName, err) + } + return InternalError("%v", err) + } + + summary := fmt.Sprintf("Change configuration of %q snap", snapName) + change := newChange(st, "configure-snap", summary, []*state.TaskSet{taskset}, []string{snapName}) + + st.EnsureBefore(0) + + return AsyncResponse(nil, &Meta{Change: change.ID()}) +} + +// interfacesConnectionsMultiplexer multiplexes to either legacy (connection) or modern behavior (interfaces). +func interfacesConnectionsMultiplexer(c *Command, r *http.Request, user *auth.UserState) Response { + query := r.URL.Query() + qselect := query.Get("select") + if qselect == "" { + return getLegacyConnections(c, r, user) + } else { + return getInterfaces(c, r, user) + } +} + +func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { + q := r.URL.Query() + pselect := q.Get("select") + if pselect != "all" && pselect != "connected" { + return BadRequest("unsupported select qualifier") + } + var names []string + namesStr := q.Get("names") + if namesStr != "" { + names = strings.Split(namesStr, ",") + } + + opts := &interfaces.InfoOptions{ + Names: names, + Doc: q.Get("doc") == "true", + Plugs: q.Get("plugs") == "true", + Slots: q.Get("slots") == "true", + Connected: pselect == "connected", + } + repo := c.d.overlord.InterfaceManager().Repository() + return SyncResponse(repo.Info(opts), nil) +} + +func getLegacyConnections(c *Command, r *http.Request, user *auth.UserState) Response { + repo := c.d.overlord.InterfaceManager().Repository() + ifaces := repo.Interfaces() + + var ifjson interfacesJSON + + plugConns := map[string][]interfaces.SlotRef{} + slotConns := map[string][]interfaces.PlugRef{} + + for _, conn := range ifaces.Connections { + plugRef := conn.PlugRef.String() + slotRef := conn.SlotRef.String() + plugConns[plugRef] = append(plugConns[plugRef], conn.SlotRef) + slotConns[slotRef] = append(slotConns[slotRef], conn.PlugRef) + } + + for _, plug := range ifaces.Plugs { + var apps []string + for _, app := range plug.Apps { + apps = append(apps, app.Name) + } + pj := plugJSON{ + Snap: plug.Snap.Name(), + Name: plug.Name, + Interface: plug.Interface, + Attrs: plug.Attrs, + Apps: apps, + Label: plug.Label, + Connections: plugConns[plug.String()], + } + ifjson.Plugs = append(ifjson.Plugs, pj) + } + for _, slot := range ifaces.Slots { + var apps []string + for _, app := range slot.Apps { + apps = append(apps, app.Name) + } + + sj := slotJSON{ + Snap: slot.Snap.Name(), + Name: slot.Name, + Interface: slot.Interface, + Attrs: slot.Attrs, + Apps: apps, + Label: slot.Label, + Connections: slotConns[slot.String()], + } + ifjson.Slots = append(ifjson.Slots, sj) + } + + return SyncResponse(ifjson, nil) +} + +// plugJSON aids in marshaling Plug into JSON. +type plugJSON struct { + Snap string `json:"snap"` + Name string `json:"plug"` + Interface string `json:"interface"` + Attrs map[string]interface{} `json:"attrs,omitempty"` + Apps []string `json:"apps,omitempty"` + Label string `json:"label"` + Connections []interfaces.SlotRef `json:"connections,omitempty"` +} + +// slotJSON aids in marshaling Slot into JSON. +type slotJSON struct { + Snap string `json:"snap"` + Name string `json:"slot"` + Interface string `json:"interface"` + Attrs map[string]interface{} `json:"attrs,omitempty"` + Apps []string `json:"apps,omitempty"` + Label string `json:"label"` + Connections []interfaces.PlugRef `json:"connections,omitempty"` +} + +// interfacesJSON aids in marshaling plugs, slots and their connections into JSON. +type interfacesJSON struct { + Plugs []plugJSON `json:"plugs,omitempty"` + Slots []slotJSON `json:"slots,omitempty"` +} + +// interfaceAction is an action performed on the interface system. +type interfaceAction struct { + Action string `json:"action"` + Plugs []plugJSON `json:"plugs,omitempty"` + Slots []slotJSON `json:"slots,omitempty"` +} + +func snapNamesFromConns(conns []interfaces.ConnRef) []string { + m := make(map[string]bool) + for _, conn := range conns { + m[conn.PlugRef.Snap] = true + m[conn.SlotRef.Snap] = true + } + l := make([]string, 0, len(m)) + for name := range m { + l = append(l, name) + } + sort.Strings(l) + return l +} + +// changeInterfaces controls the interfaces system. +// Plugs can be connected to and disconnected from slots. +// When enableInternalInterfaceActions is true plugs and slots can also be +// explicitly added and removed. +func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { + var a interfaceAction + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&a); err != nil { + return BadRequest("cannot decode request body into an interface action: %v", err) + } + if a.Action == "" { + return BadRequest("interface action not specified") + } + if !c.d.enableInternalInterfaceActions && a.Action != "connect" && a.Action != "disconnect" { + return BadRequest("internal interface actions are disabled") + } + if len(a.Plugs) > 1 || len(a.Slots) > 1 { + return NotImplemented("many-to-many operations are not implemented") + } + if a.Action != "connect" && a.Action != "disconnect" { + return BadRequest("unsupported interface action: %q", a.Action) + } + if len(a.Plugs) == 0 || len(a.Slots) == 0 { + return BadRequest("at least one plug and slot is required") + } + + var summary string + var err error + + var tasksets []*state.TaskSet + var affected []string + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + switch a.Action { + case "connect": + var connRef interfaces.ConnRef + repo := c.d.overlord.InterfaceManager().Repository() + connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + if err == nil { + var ts *state.TaskSet + summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) + ts, err = ifacestate.Connect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) + tasksets = append(tasksets, ts) + affected = snapNamesFromConns([]interfaces.ConnRef{connRef}) + } + case "disconnect": + var conns []interfaces.ConnRef + repo := c.d.overlord.InterfaceManager().Repository() + summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + conns, err = repo.ResolveDisconnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + if err == nil { + for _, connRef := range conns { + var ts *state.TaskSet + ts, err = ifacestate.Disconnect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) + if err != nil { + break + } + ts.JoinLane(st.NewLane()) + tasksets = append(tasksets, ts) + } + affected = snapNamesFromConns(conns) + } + } + if err != nil { + return BadRequest("%v", err) + } + + change := newChange(st, a.Action+"-snap", summary, tasksets, affected) + + st.EnsureBefore(0) + + return AsyncResponse(nil, &Meta{Change: change.ID()}) +} + +func getAssertTypeNames(c *Command, r *http.Request, user *auth.UserState) Response { + return SyncResponse(map[string][]string{ + "types": asserts.TypeNames(), + }, nil) +} + +func doAssert(c *Command, r *http.Request, user *auth.UserState) Response { + batch := assertstate.NewBatch() + _, err := batch.AddStream(r.Body) + if err != nil { + return BadRequest("cannot decode request body into assertions: %v", err) + } + + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + if err := batch.Commit(state); err != nil { + return BadRequest("assert failed: %v", err) + } + // TODO: what more info do we want to return on success? + return &resp{ + Type: ResponseTypeSync, + Status: 200, + } +} + +func assertsFindMany(c *Command, r *http.Request, user *auth.UserState) Response { + assertTypeName := muxVars(r)["assertType"] + assertType := asserts.Type(assertTypeName) + if assertType == nil { + return BadRequest("invalid assert type: %q", assertTypeName) + } + headers := map[string]string{} + q := r.URL.Query() + for k := range q { + headers[k] = q.Get(k) + } + + state := c.d.overlord.State() + state.Lock() + db := assertstate.DB(state) + state.Unlock() + + assertions, err := db.FindMany(assertType, headers) + if asserts.IsNotFound(err) { + return AssertResponse(nil, true) + } else if err != nil { + return InternalError("searching assertions failed: %v", err) + } + return AssertResponse(assertions, true) +} + +type changeInfo struct { + ID string `json:"id"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Tasks []*taskInfo `json:"tasks,omitempty"` + Ready bool `json:"ready"` + Err string `json:"err,omitempty"` + + SpawnTime time.Time `json:"spawn-time,omitempty"` + ReadyTime *time.Time `json:"ready-time,omitempty"` + + Data map[string]*json.RawMessage `json:"data,omitempty"` +} + +type taskInfo struct { + ID string `json:"id"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Log []string `json:"log,omitempty"` + Progress taskInfoProgress `json:"progress"` + + SpawnTime time.Time `json:"spawn-time,omitempty"` + ReadyTime *time.Time `json:"ready-time,omitempty"` +} + +type taskInfoProgress struct { + Label string `json:"label"` + Done int `json:"done"` + Total int `json:"total"` +} + +func change2changeInfo(chg *state.Change) *changeInfo { + status := chg.Status() + chgInfo := &changeInfo{ + ID: chg.ID(), + Kind: chg.Kind(), + Summary: chg.Summary(), + Status: status.String(), + Ready: status.Ready(), + + SpawnTime: chg.SpawnTime(), + } + readyTime := chg.ReadyTime() + if !readyTime.IsZero() { + chgInfo.ReadyTime = &readyTime + } + if err := chg.Err(); err != nil { + chgInfo.Err = err.Error() + } + + tasks := chg.Tasks() + taskInfos := make([]*taskInfo, len(tasks)) + for j, t := range tasks { + label, done, total := t.Progress() + + taskInfo := &taskInfo{ + ID: t.ID(), + Kind: t.Kind(), + Summary: t.Summary(), + Status: t.Status().String(), + Log: t.Log(), + Progress: taskInfoProgress{ + Label: label, + Done: done, + Total: total, + }, + SpawnTime: t.SpawnTime(), + } + readyTime := t.ReadyTime() + if !readyTime.IsZero() { + taskInfo.ReadyTime = &readyTime + } + taskInfos[j] = taskInfo + } + chgInfo.Tasks = taskInfos + + var data map[string]*json.RawMessage + if chg.Get("api-data", &data) == nil { + chgInfo.Data = data + } + + return chgInfo +} + +func getChange(c *Command, r *http.Request, user *auth.UserState) Response { + chID := muxVars(r)["id"] + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + chg := state.Change(chID) + if chg == nil { + return NotFound("cannot find change with id %q", chID) + } + + return SyncResponse(change2changeInfo(chg), nil) +} + +func getChanges(c *Command, r *http.Request, user *auth.UserState) Response { + query := r.URL.Query() + qselect := query.Get("select") + if qselect == "" { + qselect = "in-progress" + } + var filter func(*state.Change) bool + switch qselect { + case "all": + filter = func(*state.Change) bool { return true } + case "in-progress": + filter = func(chg *state.Change) bool { return !chg.Status().Ready() } + case "ready": + filter = func(chg *state.Change) bool { return chg.Status().Ready() } + default: + return BadRequest("select should be one of: all,in-progress,ready") + } + + if wantedName := query.Get("for"); wantedName != "" { + outerFilter := filter + filter = func(chg *state.Change) bool { + if !outerFilter(chg) { + return false + } + + var snapNames []string + if err := chg.Get("snap-names", &snapNames); err != nil { + logger.Noticef("Cannot get snap-name for change %v", chg.ID()) + return false + } + + for _, snapName := range snapNames { + if snapName == wantedName { + return true + } + } + + return false + } + } + + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + chgs := state.Changes() + chgInfos := make([]*changeInfo, 0, len(chgs)) + for _, chg := range chgs { + if !filter(chg) { + continue + } + chgInfos = append(chgInfos, change2changeInfo(chg)) + } + return SyncResponse(chgInfos, nil) +} + +func abortChange(c *Command, r *http.Request, user *auth.UserState) Response { + chID := muxVars(r)["id"] + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + chg := state.Change(chID) + if chg == nil { + return NotFound("cannot find change with id %q", chID) + } + + var reqData struct { + Action string `json:"action"` + } + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&reqData); err != nil { + return BadRequest("cannot decode data from request body: %v", err) + } + + if reqData.Action != "abort" { + return BadRequest("change action %q is unsupported", reqData.Action) + } + + if chg.Status().Ready() { + return BadRequest("cannot abort change %s with nothing pending", chID) + } + + // flag the change + chg.Abort() + + // actually ask to proceed with the abort + ensureStateSoon(state) + + return SyncResponse(change2changeInfo(chg), nil) +} + +var ( + postCreateUserUcrednetGet = ucrednetGet + storeUserInfo = store.UserInfo + osutilAddUser = osutil.AddUser +) + +func getUserDetailsFromStore(email string) (string, *osutil.AddUserOptions, error) { + v, err := storeUserInfo(email) + if err != nil { + return "", nil, fmt.Errorf("cannot create user %q: %s", email, err) + } + if len(v.SSHKeys) == 0 { + return "", nil, fmt.Errorf("cannot create user for %q: no ssh keys found", email) + } + + gecos := fmt.Sprintf("%s,%s", email, v.OpenIDIdentifier) + opts := &osutil.AddUserOptions{ + SSHKeys: v.SSHKeys, + Gecos: gecos, + } + return v.Username, opts, nil +} + +func createAllKnownSystemUsers(st *state.State, createData *postUserCreateData) Response { + var createdUsers []userResponseData + + st.Lock() + db := assertstate.DB(st) + modelAs, err := devicestate.Model(st) + st.Unlock() + if err != nil { + return InternalError("cannot get model assertion") + } + + headers := map[string]string{ + "brand-id": modelAs.BrandID(), + } + st.Lock() + assertions, err := db.FindMany(asserts.SystemUserType, headers) + st.Unlock() + if err != nil && !asserts.IsNotFound(err) { + return BadRequest("cannot find system-user assertion: %s", err) + } + + for _, as := range assertions { + email := as.(*asserts.SystemUser).Email() + // we need to use getUserDetailsFromAssertion as this verifies + // the assertion against the current brand/model/time + username, opts, err := getUserDetailsFromAssertion(st, email) + if err != nil { + logger.Noticef("ignoring system-user assertion for %q: %s", email, err) + continue + } + // ignore already existing users + if _, err := user.Lookup(username); err == nil { + continue + } + + // FIXME: duplicated code + opts.Sudoer = createData.Sudoer + opts.ExtraUsers = !release.OnClassic + + if err := osutilAddUser(username, opts); err != nil { + return InternalError("cannot add user %q: %s", username, err) + } + if err := setupLocalUser(st, username, email); err != nil { + return InternalError("%s", err) + } + createdUsers = append(createdUsers, userResponseData{ + Username: username, + SSHKeys: opts.SSHKeys, + }) + } + + return SyncResponse(createdUsers, nil) +} + +func getUserDetailsFromAssertion(st *state.State, email string) (string, *osutil.AddUserOptions, error) { + errorPrefix := fmt.Sprintf("cannot add system-user %q: ", email) + + st.Lock() + db := assertstate.DB(st) + modelAs, err := devicestate.Model(st) + st.Unlock() + if err != nil { + return "", nil, fmt.Errorf(errorPrefix+"cannot get model assertion: %s", err) + } + + brandID := modelAs.BrandID() + series := modelAs.Series() + model := modelAs.Model() + + a, err := db.Find(asserts.SystemUserType, map[string]string{ + "brand-id": brandID, + "email": email, + }) + if err != nil { + return "", nil, fmt.Errorf(errorPrefix+"%v", err) + } + // the asserts package guarantees that this cast will work + su := a.(*asserts.SystemUser) + + // cross check that the assertion is valid for the given series/model + + // check that the signer of the assertion is one of the accepted ones + sysUserAuths := modelAs.SystemUserAuthority() + if len(sysUserAuths) > 0 && !strutil.ListContains(sysUserAuths, su.AuthorityID()) { + return "", nil, fmt.Errorf(errorPrefix+"%q not in accepted authorities %q", email, su.AuthorityID(), sysUserAuths) + } + if len(su.Series()) > 0 && !strutil.ListContains(su.Series(), series) { + return "", nil, fmt.Errorf(errorPrefix+"%q not in series %q", email, series, su.Series()) + } + if len(su.Models()) > 0 && !strutil.ListContains(su.Models(), model) { + return "", nil, fmt.Errorf(errorPrefix+"%q not in models %q", model, su.Models()) + } + if !su.ValidAt(time.Now()) { + return "", nil, fmt.Errorf(errorPrefix + "assertion not valid anymore") + } + + gecos := fmt.Sprintf("%s,%s", email, su.Name()) + opts := &osutil.AddUserOptions{ + SSHKeys: su.SSHKeys(), + Gecos: gecos, + Password: su.Password(), + } + return su.Username(), opts, nil +} + +type postUserCreateData struct { + Email string `json:"email"` + Sudoer bool `json:"sudoer"` + Known bool `json:"known"` + ForceManaged bool `json:"force-managed"` +} + +var userLookup = user.Lookup + +func setupLocalUser(st *state.State, username, email string) error { + user, err := userLookup(username) + if err != nil { + return fmt.Errorf("cannot lookup user %q: %s", username, err) + } + uid, err := strconv.Atoi(user.Uid) + if err != nil { + return fmt.Errorf("cannot get uid of user %q: %s", username, err) + } + gid, err := strconv.Atoi(user.Gid) + if err != nil { + return fmt.Errorf("cannot get gid of user %q: %s", username, err) + } + authDataFn := filepath.Join(user.HomeDir, ".snap", "auth.json") + if err := osutil.MkdirAllChown(filepath.Dir(authDataFn), 0700, uid, gid); err != nil { + return err + } + + // setup new user, local-only + st.Lock() + authUser, err := auth.NewUser(st, username, email, "", nil) + st.Unlock() + if err != nil { + return fmt.Errorf("cannot persist authentication details: %v", err) + } + // store macaroon auth in auth.json in the new users home dir + outStr, err := json.Marshal(struct { + Macaroon string `json:"macaroon"` + }{ + Macaroon: authUser.Macaroon, + }) + if err != nil { + return fmt.Errorf("cannot marshal auth data: %s", err) + } + if err := osutil.AtomicWriteFileChown(authDataFn, []byte(outStr), 0600, 0, uid, gid); err != nil { + return fmt.Errorf("cannot write auth file %q: %s", authDataFn, err) + } + + return nil +} + +func postCreateUser(c *Command, r *http.Request, user *auth.UserState) Response { + _, uid, err := postCreateUserUcrednetGet(r.RemoteAddr) + if err != nil { + return BadRequest("cannot get ucrednet uid: %v", err) + } + if uid != 0 { + return BadRequest("cannot use create-user as non-root") + } + + var createData postUserCreateData + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&createData); err != nil { + return BadRequest("cannot decode create-user data from request body: %v", err) + } + + // verify request + st := c.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + st.Unlock() + if err != nil { + return InternalError("cannot get user count: %s", err) + } + + if !createData.ForceManaged { + if len(users) > 0 { + return BadRequest("cannot create user: device already managed") + } + if release.OnClassic { + return BadRequest("cannot create user: device is a classic system") + } + } + + // special case: the user requested the creation of all known + // system-users + if createData.Email == "" && createData.Known { + return createAllKnownSystemUsers(c.d.overlord.State(), &createData) + } + if createData.Email == "" { + return BadRequest("cannot create user: 'email' field is empty") + } + + var username string + var opts *osutil.AddUserOptions + if createData.Known { + username, opts, err = getUserDetailsFromAssertion(st, createData.Email) + } else { + username, opts, err = getUserDetailsFromStore(createData.Email) + } + if err != nil { + return BadRequest("%s", err) + } + + // FIXME: duplicated code + opts.Sudoer = createData.Sudoer + opts.ExtraUsers = !release.OnClassic + + if err := osutilAddUser(username, opts); err != nil { + return BadRequest("cannot create user %s: %s", username, err) + } + + if err := setupLocalUser(c.d.overlord.State(), username, createData.Email); err != nil { + return InternalError("%s", err) + } + + return SyncResponse(&userResponseData{ + Username: username, + SSHKeys: opts.SSHKeys, + }, nil) +} + +func convertBuyError(err error) Response { + switch err { + case nil: + return nil + case store.ErrInvalidCredentials: + return Unauthorized(err.Error()) + case store.ErrUnauthenticated: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindLoginRequired, + }, + Status: 400, + }, nil) + case store.ErrTOSNotAccepted: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindTermsNotAccepted, + }, + Status: 400, + }, nil) + case store.ErrNoPaymentMethods: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindNoPaymentMethods, + }, + Status: 400, + }, nil) + case store.ErrPaymentDeclined: + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindPaymentDeclined, + }, + Status: 400, + }, nil) + default: + return InternalError("%v", err) + } +} + +type debugAction struct { + Action string `json:"action"` +} + +func postDebug(c *Command, r *http.Request, user *auth.UserState) Response { + var a debugAction + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&a); err != nil { + return BadRequest("cannot decode request body into a debug action: %v", err) + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + switch a.Action { + case "ensure-state-soon": + ensureStateSoon(st) + return SyncResponse(true, nil) + case "get-base-declaration": + bd, err := assertstate.BaseDeclaration(st) + if err != nil { + return InternalError("cannot get base declaration: %s", err) + } + return SyncResponse(map[string]interface{}{ + "base-declaration": string(asserts.Encode(bd)), + }, nil) + case "can-manage-refreshes": + return SyncResponse(devicestate.CanManageRefreshes(st), nil) + default: + return BadRequest("unknown debug action: %v", a.Action) + } +} + +func postBuy(c *Command, r *http.Request, user *auth.UserState) Response { + var opts store.BuyOptions + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&opts) + if err != nil { + return BadRequest("cannot decode buy options from request body: %v", err) + } + + s := getStore(c) + + buyResult, err := s.Buy(&opts, user) + + if resp := convertBuyError(err); resp != nil { + return resp + } + + return SyncResponse(buyResult, nil) +} + +func readyToBuy(c *Command, r *http.Request, user *auth.UserState) Response { + s := getStore(c) + + if resp := convertBuyError(s.ReadyToBuy(user)); resp != nil { + return resp + } + + return SyncResponse(true, nil) +} + +func runSnapctl(c *Command, r *http.Request, user *auth.UserState) Response { + var snapctlOptions client.SnapCtlOptions + if err := jsonutil.DecodeWithNumber(r.Body, &snapctlOptions); err != nil { + return BadRequest("cannot decode snapctl request: %s", err) + } + + if len(snapctlOptions.Args) == 0 { + return BadRequest("snapctl cannot run without args") + } + + // Ignore missing context error to allow 'snapctl -h' without a context; + // Actual context is validated later by get/set. + context, _ := c.d.overlord.HookManager().Context(snapctlOptions.ContextID) + stdout, stderr, err := ctlcmd.Run(context, snapctlOptions.Args) + if err != nil { + if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { + stdout = []byte(e.Error()) + } else { + return BadRequest("error running snapctl: %s", err) + } + } + + if context != nil && context.IsEphemeral() { + context.Lock() + defer context.Unlock() + if err := context.Done(); err != nil { + return BadRequest(i18n.G("set failed: %v"), err) + } + } + + result := map[string]string{ + "stdout": string(stdout), + "stderr": string(stderr), + } + + return SyncResponse(result, nil) +} + +func getUsers(c *Command, r *http.Request, user *auth.UserState) Response { + _, uid, err := postCreateUserUcrednetGet(r.RemoteAddr) + if err != nil { + return BadRequest("cannot get ucrednet uid: %v", err) + } + if uid != 0 { + return BadRequest("cannot get users as non-root") + } + + st := c.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + st.Unlock() + if err != nil { + return InternalError("cannot get users: %s", err) + } + + resp := make([]userResponseData, len(users)) + for i, u := range users { + resp[i] = userResponseData{ + Username: u.Username, + Email: u.Email, + ID: u.ID, + } + } + return SyncResponse(resp, nil) +} + +// aliasAction is an action performed on aliases +type aliasAction struct { + Action string `json:"action"` + Snap string `json:"snap"` + App string `json:"app"` + Alias string `json:"alias"` + // old now unsupported api + Aliases []string `json:"aliases"` +} + +func changeAliases(c *Command, r *http.Request, user *auth.UserState) Response { + var a aliasAction + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&a); err != nil { + return BadRequest("cannot decode request body into an alias action: %v", err) + } + if len(a.Aliases) != 0 { + return BadRequest("cannot interpret request, snaps can no longer be expected to declare their aliases") + } + + var taskset *state.TaskSet + var err error + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + switch a.Action { + default: + return BadRequest("unsupported alias action: %q", a.Action) + case "alias": + taskset, err = snapstate.Alias(st, a.Snap, a.App, a.Alias) + case "unalias": + if a.Alias == a.Snap { + // Do What I mean: + // check if a snap is referred/intended + // or just an alias + var snapst snapstate.SnapState + err := snapstate.Get(st, a.Snap, &snapst) + if err != nil && err != state.ErrNoState { + return InternalError("%v", err) + } + if err == state.ErrNoState { // not a snap + a.Snap = "" + } + } + if a.Snap != "" { + a.Alias = "" + taskset, err = snapstate.DisableAllAliases(st, a.Snap) + } else { + taskset, a.Snap, err = snapstate.RemoveManualAlias(st, a.Alias) + } + case "prefer": + taskset, err = snapstate.Prefer(st, a.Snap) + } + if err != nil { + return BadRequest("%v", err) + } + + var summary string + switch a.Action { + case "alias": + summary = fmt.Sprintf(i18n.G("Setup alias %q => %q for snap %q"), a.Alias, a.App, a.Snap) + case "unalias": + if a.Alias != "" { + summary = fmt.Sprintf(i18n.G("Remove manual alias %q for snap %q"), a.Alias, a.Snap) + } else { + summary = fmt.Sprintf(i18n.G("Disable all aliases for snap %q"), a.Snap) + } + case "prefer": + summary = fmt.Sprintf(i18n.G("Prefer aliases of snap %q"), a.Snap) + } + + change := newChange(st, a.Action, summary, []*state.TaskSet{taskset}, []string{a.Snap}) + st.EnsureBefore(0) + + return AsyncResponse(nil, &Meta{Change: change.ID()}) +} + +type aliasStatus struct { + Command string `json:"command"` + Status string `json:"status"` + Manual string `json:"manual,omitempty"` + Auto string `json:"auto,omitempty"` +} + +// getAliases produces a response with a map snap -> alias -> aliasStatus +func getAliases(c *Command, r *http.Request, user *auth.UserState) Response { + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + + res := make(map[string]map[string]aliasStatus) + + allStates, err := snapstate.All(state) + if err != nil { + return InternalError("cannot list local snaps: %v", err) + } + + for snapName, snapst := range allStates { + if err != nil { + return InternalError("cannot retrieve info for snap %q: %v", snapName, err) + } + if len(snapst.Aliases) != 0 { + snapAliases := make(map[string]aliasStatus) + res[snapName] = snapAliases + autoDisabled := snapst.AutoAliasesDisabled + for alias, aliasTarget := range snapst.Aliases { + aliasStatus := aliasStatus{ + Manual: aliasTarget.Manual, + Auto: aliasTarget.Auto, + } + status := "auto" + tgt := aliasTarget.Effective(autoDisabled) + if tgt == "" { + status = "disabled" + tgt = aliasTarget.Auto + } else if aliasTarget.Manual != "" { + status = "manual" + } + aliasStatus.Status = status + aliasStatus.Command = snap.JoinSnapApp(snapName, tgt) + snapAliases[alias] = aliasStatus + } + } + } + + return SyncResponse(res, nil) +} + +func getAppsInfo(c *Command, r *http.Request, user *auth.UserState) Response { + query := r.URL.Query() + + opts := appInfoOptions{} + switch sel := query.Get("select"); sel { + case "": + // nothing to do + case "service": + opts.service = true + default: + return BadRequest("invalid select parameter: %q", sel) + } + + appInfos, rsp := appInfosFor(c.d.overlord.State(), splitQS(query.Get("names")), opts) + if rsp != nil { + return rsp + } + + return SyncResponse(clientAppInfosFromSnapAppInfos(appInfos), nil) +} + +func getLogs(c *Command, r *http.Request, user *auth.UserState) Response { + query := r.URL.Query() + n := "10" + if s := query.Get("n"); s != "" { + m, err := strconv.ParseInt(s, 0, 32) + if err != nil { + return BadRequest(`invalid value for n: %q: %v`, s, err) + } + if m < 0 { + n = "all" + } else { + n = s + } + } + follow := false + if s := query.Get("follow"); s != "" { + f, err := strconv.ParseBool(s) + if err != nil { + return BadRequest(`invalid value for follow: %q: %v`, s, err) + } + follow = f + } + + // only services have logs for now + opts := appInfoOptions{service: true} + appInfos, rsp := appInfosFor(c.d.overlord.State(), splitQS(query.Get("names")), opts) + if rsp != nil { + return rsp + } + if len(appInfos) == 0 { + return AppNotFound("no matching services") + } + + serviceNames := make([]string, len(appInfos)) + for i, appInfo := range appInfos { + serviceNames[i] = appInfo.ServiceName() + } + + sysd := systemd.New(dirs.GlobalRootDir, progress.Null) + reader, err := sysd.LogReader(serviceNames, n, follow) + if err != nil { + return InternalError("cannot get logs: %v", err) + } + + return &journalLineReaderSeqResponse{ + ReadCloser: reader, + follow: follow, + } +} + +func postApps(c *Command, r *http.Request, user *auth.UserState) Response { + var inst servicestate.Instruction + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&inst); err != nil { + return BadRequest("cannot decode request body into service operation: %v", err) + } + if len(inst.Names) == 0 { + // on POST, don't allow empty to mean all + return BadRequest("cannot perform operation on services without a list of services to operate on") + } + + st := c.d.overlord.State() + appInfos, rsp := appInfosFor(st, inst.Names, appInfoOptions{service: true}) + if rsp != nil { + return rsp + } + if len(appInfos) == 0 { + // can't happen: appInfosFor with a non-empty list of services + // shouldn't ever return an empty appInfos with no error response + return InternalError("no services found") + } + + ts, err := servicestate.Control(st, appInfos, &inst, nil) + if err != nil { + if _, ok := err.(servicestate.ServiceActionConflictError); ok { + return Conflict(err.Error()) + } + return BadRequest(err.Error()) + } + st.Lock() + defer st.Unlock() + chg := newChange(st, "service-control", fmt.Sprintf("Running service command"), []*state.TaskSet{ts}, inst.Names) + st.EnsureBefore(0) + return AsyncResponse(nil, &Meta{Change: chg.ID()}) +} diff --git a/daemon/api_mock_test.go b/daemon/api_mock_test.go new file mode 100644 index 00000000..3db7aacd --- /dev/null +++ b/daemon/api_mock_test.go @@ -0,0 +1,124 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" +) + +func (s *apiSuite) mockSnap(c *C, yamlText string) *snap.Info { + if s.d == nil { + panic("call s.daemon(c) in your test first") + } + + snapInfo := snaptest.MockSnap(c, yamlText, "", &snap.SideInfo{Revision: snap.R(1)}) + + st := s.d.overlord.State() + + st.Lock() + defer st.Unlock() + + // Put a side info into the state + snapstate.Set(st, snapInfo.Name(), &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + { + RealName: snapInfo.Name(), + Revision: snapInfo.Revision, + SnapID: "ididid", + }, + }, + Current: snapInfo.Revision, + }) + + // Put the snap into the interface repository + repo := s.d.overlord.InterfaceManager().Repository() + err := repo.AddSnap(snapInfo) + c.Assert(err, IsNil) + return snapInfo +} + +func (s *apiSuite) mockIface(c *C, iface interfaces.Interface) { + if s.d == nil { + panic("call s.daemon(c) in your test first") + } + err := s.d.overlord.InterfaceManager().Repository().AddInterface(iface) + c.Assert(err, IsNil) +} + +var simpleYaml = ` +name: simple +version: 1 +` + +var consumerYaml = ` +name: consumer +version: 1 +apps: + app: +plugs: + plug: + interface: test + key: value + label: label +` + +var producerYaml = ` +name: producer +version: 1 +apps: + app: +slots: + slot: + interface: test + key: value + label: label +` + +var differentProducerYaml = ` +name: producer +version: 1 +apps: + app: +slots: + slot: + interface: different + key: value + label: label +` + +var configYaml = ` +name: config-snap +version: 1 +hooks: + configure: +` +var aliasYaml = ` +name: alias-snap +version: 1 +apps: + app: + app2: +` diff --git a/daemon/api_test.go b/daemon/api_test.go new file mode 100644 index 00000000..bb8ec62f --- /dev/null +++ b/daemon/api_test.go @@ -0,0 +1,6292 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "bytes" + "crypto" + "encoding/json" + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "io/ioutil" + "math" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/url" + "os" + "os/user" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "golang.org/x/crypto/sha3" + "gopkg.in/check.v1" + "gopkg.in/macaroon.v1" + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord" + "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/servicestate" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/store/storetest" + "github.com/snapcore/snapd/systemd" + "github.com/snapcore/snapd/testutil" +) + +type apiBaseSuite struct { + storetest.Store + + rsnaps []*snap.Info + err error + vars map[string]string + storeSearch store.Search + suggestedCurrency string + d *Daemon + user *auth.UserState + restoreBackends func() + refreshCandidates []*store.RefreshCandidate + buyOptions *store.BuyOptions + buyResult *store.BuyResult + storeSigning *assertstest.StoreStack + restoreRelease func() + trustedRestorer func() + + systemctlRestorer func() + sysctlArgses [][]string + sysctlBufs [][]byte + sysctlErrs []error + + journalctlRestorer func() + jctlSvcses [][]string + jctlNs []string + jctlFollows []bool + jctlRCs []io.ReadCloser + jctlErrs []error +} + +func (s *apiBaseSuite) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { + s.user = user + if !spec.AnyChannel { + return nil, fmt.Errorf("api is expected to set AnyChannel") + } + if len(s.rsnaps) > 0 { + return s.rsnaps[0], s.err + } + return nil, s.err +} + +func (s *apiBaseSuite) Find(search *store.Search, user *auth.UserState) ([]*snap.Info, error) { + s.storeSearch = *search + s.user = user + + return s.rsnaps, s.err +} + +func (s *apiBaseSuite) LookupRefresh(snap *store.RefreshCandidate, user *auth.UserState) (*snap.Info, error) { + s.refreshCandidates = []*store.RefreshCandidate{snap} + s.user = user + + return s.rsnaps[0], s.err +} + +func (s *apiBaseSuite) ListRefresh(snaps []*store.RefreshCandidate, user *auth.UserState, flags *store.RefreshOptions) ([]*snap.Info, error) { + s.refreshCandidates = snaps + s.user = user + + return s.rsnaps, s.err +} + +func (s *apiBaseSuite) SuggestedCurrency() string { + return s.suggestedCurrency +} + +func (s *apiBaseSuite) Buy(options *store.BuyOptions, user *auth.UserState) (*store.BuyResult, error) { + s.buyOptions = options + s.user = user + return s.buyResult, s.err +} + +func (s *apiBaseSuite) ReadyToBuy(user *auth.UserState) error { + s.user = user + return s.err +} + +func (s *apiBaseSuite) muxVars(*http.Request) map[string]string { + return s.vars +} + +func (s *apiBaseSuite) SetUpSuite(c *check.C) { + muxVars = s.muxVars + s.restoreRelease = release.MockForcedDevmode(false) + s.systemctlRestorer = systemd.MockSystemctl(s.systemctl) + s.journalctlRestorer = systemd.MockJournalctl(s.journalctl) +} + +func (s *apiBaseSuite) TearDownSuite(c *check.C) { + muxVars = nil + s.restoreRelease() + s.systemctlRestorer() + s.journalctlRestorer() +} + +func (s *apiBaseSuite) systemctl(args ...string) (buf []byte, err error) { + s.sysctlArgses = append(s.sysctlArgses, args) + + if args[0] != "show" && args[0] != "start" && args[0] != "stop" && args[0] != "restart" { + panic(fmt.Sprintf("unexpected systemctl call: %v", args)) + } + + if len(s.sysctlErrs) > 0 { + err, s.sysctlErrs = s.sysctlErrs[0], s.sysctlErrs[1:] + } + if len(s.sysctlBufs) > 0 { + buf, s.sysctlBufs = s.sysctlBufs[0], s.sysctlBufs[1:] + } + + return buf, err +} + +func (s *apiBaseSuite) journalctl(svcs []string, n string, follow bool) (rc io.ReadCloser, err error) { + s.jctlSvcses = append(s.jctlSvcses, svcs) + s.jctlNs = append(s.jctlNs, n) + s.jctlFollows = append(s.jctlFollows, follow) + + if len(s.jctlErrs) > 0 { + err, s.jctlErrs = s.jctlErrs[0], s.jctlErrs[1:] + } + if len(s.jctlRCs) > 0 { + rc, s.jctlRCs = s.jctlRCs[0], s.jctlRCs[1:] + } + + return rc, err +} + +func (s *apiBaseSuite) SetUpTest(c *check.C) { + s.sysctlArgses = nil + s.sysctlBufs = nil + s.sysctlErrs = nil + s.jctlSvcses = nil + s.jctlNs = nil + s.jctlFollows = nil + s.jctlRCs = nil + s.jctlErrs = nil + + dirs.SetRootDir(c.MkDir()) + err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) + c.Assert(err, check.IsNil) + c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), check.IsNil) + + s.rsnaps = nil + s.suggestedCurrency = "" + s.storeSearch = store.Search{} + s.err = nil + s.vars = nil + s.user = nil + s.d = nil + s.refreshCandidates = nil + // Disable real security backends for all API tests + s.restoreBackends = ifacestate.MockSecurityBackends(nil) + + s.buyOptions = nil + s.buyResult = nil + + s.storeSigning = assertstest.NewStoreStack("can0nical", nil) + s.trustedRestorer = sysdb.InjectTrusted(s.storeSigning.Trusted) + + assertstateRefreshSnapDeclarations = nil + snapstateInstall = nil + snapstateInstallMany = nil + snapstateInstallPath = nil + snapstateRefreshCandidates = nil + snapstateRemoveMany = nil + snapstateRevert = nil + snapstateRevertToRevision = nil + snapstateTryPath = nil + snapstateUpdate = nil + snapstateUpdateMany = nil +} + +func (s *apiBaseSuite) TearDownTest(c *check.C) { + s.trustedRestorer() + s.d = nil + s.restoreBackends() + unsafeReadSnapInfo = unsafeReadSnapInfoImpl + ensureStateSoon = ensureStateSoonImpl + dirs.SetRootDir("") + + assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations + snapstateInstall = snapstate.Install + snapstateInstallMany = snapstate.InstallMany + snapstateInstallPath = snapstate.InstallPath + snapstateRefreshCandidates = snapstate.RefreshCandidates + snapstateRemoveMany = snapstate.RemoveMany + snapstateRevert = snapstate.Revert + snapstateRevertToRevision = snapstate.RevertToRevision + snapstateTryPath = snapstate.TryPath + snapstateUpdate = snapstate.Update + snapstateUpdateMany = snapstate.UpdateMany +} + +func (s *apiBaseSuite) daemon(c *check.C) *Daemon { + if s.d != nil { + panic("called daemon() twice") + } + d, err := New() + c.Assert(err, check.IsNil) + d.addRoutes() + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + snapstate.ReplaceStore(st, s) + // mark as already seeded + st.Set("seeded", true) + // registered + auth.SetDevice(st, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + Serial: "serialserial", + }) + + // don't actually try to talk to the store on snapstate.Ensure + // needs doing after the call to devicestate.Manager (which + // happens in daemon.New via overlord.New) + snapstate.CanAutoRefresh = nil + + s.d = d + return d +} + +func (s *apiBaseSuite) daemonWithOverlordMock(c *check.C) *Daemon { + if s.d != nil { + panic("called daemon() twice") + } + d, err := New() + c.Assert(err, check.IsNil) + d.addRoutes() + + o := overlord.Mock() + d.overlord = o + + st := d.overlord.State() + // adds an assertion db + assertstate.Manager(st) + st.Lock() + defer st.Unlock() + snapstate.ReplaceStore(st, s) + + s.d = d + return d +} + +type fakeSnapManager struct { + runner *state.TaskRunner +} + +func newFakeSnapManager(st *state.State) *fakeSnapManager { + runner := state.NewTaskRunner(st) + + runner.AddHandler("fake-install-snap", func(t *state.Task, _ *tomb.Tomb) error { + return nil + }, nil) + runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error { + return fmt.Errorf("fake-install-snap-error errored") + }, nil) + + return &fakeSnapManager{runner: runner} +} + +func (m *fakeSnapManager) Ensure() error { + m.runner.Ensure() + return nil +} + +func (m *fakeSnapManager) Wait() { + m.runner.Wait() +} + +func (m *fakeSnapManager) Stop() { + m.runner.Stop() +} + +// sanity +var _ overlord.StateManager = (*fakeSnapManager)(nil) + +func (s *apiBaseSuite) daemonWithFakeSnapManager(c *check.C) *Daemon { + d := s.daemonWithOverlordMock(c) + st := d.overlord.State() + d.overlord.AddManager(newFakeSnapManager(st)) + return d +} + +func (s *apiBaseSuite) waitTrivialChange(c *check.C, chg *state.Change) { + err := s.d.overlord.Settle(5 * time.Second) + c.Assert(err, check.IsNil) + c.Assert(chg.IsReady(), check.Equals, true) +} + +func (s *apiBaseSuite) mkInstalled(c *check.C, name, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info { + return s.mkInstalledInState(c, nil, name, developer, version, revision, active, extraYaml) +} + +func (s *apiBaseSuite) mkInstalledDesktopFile(c *check.C, name, content string) string { + df := filepath.Join(dirs.SnapDesktopFilesDir, name) + err := os.MkdirAll(filepath.Dir(df), 0755) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(df, []byte(content), 0644) + c.Assert(err, check.IsNil) + return df +} + +func (s *apiBaseSuite) mkInstalledInState(c *check.C, daemon *Daemon, name, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info { + snapID := name + "-id" + // Collect arguments into a snap.SideInfo structure + sideInfo := &snap.SideInfo{ + SnapID: snapID, + RealName: name, + Revision: revision, + Channel: "stable", + } + + // Collect other arguments into a yaml string + yamlText := fmt.Sprintf(` +name: %s +version: %s +%s`, name, version, extraYaml) + contents := "" + + // Mock the snap on disk + snapInfo := snaptest.MockSnap(c, yamlText, contents, sideInfo) + + c.Assert(os.MkdirAll(snapInfo.DataDir(), 0755), check.IsNil) + metadir := filepath.Join(snapInfo.MountDir(), "meta") + guidir := filepath.Join(metadir, "gui") + c.Assert(os.MkdirAll(guidir, 0755), check.IsNil) + c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil) + + if daemon != nil { + st := daemon.overlord.State() + st.Lock() + defer st.Unlock() + + err := assertstate.Add(st, s.storeSigning.StoreAccountKey("")) + if _, ok := err.(*asserts.RevisionError); !ok { + c.Assert(err, check.IsNil) + } + + devAcct := assertstest.NewAccount(s.storeSigning, developer, map[string]interface{}{ + "account-id": developer + "-id", + }, "") + err = assertstate.Add(st, devAcct) + if _, ok := err.(*asserts.RevisionError); !ok { + c.Assert(err, check.IsNil) + } + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": snapID, + "snap-name": name, + "publisher-id": devAcct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + err = assertstate.Add(st, snapDecl) + if _, ok := err.(*asserts.RevisionError); !ok { + c.Assert(err, check.IsNil) + } + + h := sha3.Sum384([]byte(fmt.Sprintf("%s%s", name, revision))) + dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:]) + c.Assert(err, check.IsNil) + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ + "snap-sha3-384": string(dgst), + "snap-size": "999", + "snap-id": snapID, + "snap-revision": fmt.Sprintf("%s", revision), + "developer-id": devAcct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + err = assertstate.Add(st, snapRev) + c.Assert(err, check.IsNil) + + var snapst snapstate.SnapState + snapstate.Get(st, name, &snapst) + snapst.Active = active + snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo) + snapst.Current = snapInfo.SideInfo.Revision + snapst.Channel = "stable" + + snapstate.Set(st, name, &snapst) + } + + return snapInfo +} + +func (s *apiBaseSuite) mkGadget(c *check.C, store string) { + yamlText := fmt.Sprintf(`name: test +version: 1 +type: gadget +gadget: {store: {id: %q}} +`, store) + contents := "" + snaptest.MockSnap(c, yamlText, contents, &snap.SideInfo{Revision: snap.R(1)}) + c.Assert(os.Symlink("1", filepath.Join(dirs.SnapMountDir, "test", "current")), check.IsNil) +} + +type apiSuite struct { + apiBaseSuite +} + +var _ = check.Suite(&apiSuite{}) + +func (s *apiSuite) TestSnapInfoOneIntegration(c *check.C) { + d := s.daemon(c) + s.vars = map[string]string{"name": "foo"} + + // we have v0 [r5] installed + s.mkInstalledInState(c, d, "foo", "bar", "v0", snap.R(5), false, "") + // and v1 [r10] is current + s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, `title: title +description: description +summary: summary +license: GPL-3.0 +apps: + cmd: + command: some.cmd + cmd2: + command: other.cmd + svc1: + command: somed1 + daemon: simple + svc2: + command: somed2 + daemon: forking + svc3: + command: somed3 + daemon: oneshot + svc4: + command: somed4 + daemon: notify +`) + df := s.mkInstalledDesktopFile(c, "foo_cmd.desktop", "[Desktop]\nExec=foo.cmd %U") + s.sysctlBufs = [][]byte{ + []byte(`Type=simple +Id=snap.foo.svc1.service +ActiveState=fumbling +UnitFileState=enabled +`), + []byte(`Type=forking +Id=snap.foo.svc2.service +ActiveState=active +UnitFileState=disabled +`), + []byte(`Type=oneshot +Id=snap.foo.svc3.service +ActiveState=reloading +UnitFileState=static +`), + []byte(`Type=notify +Id=snap.foo.svc4.service +ActiveState=inactive +UnitFileState=potatoes +`), + } + + var snapst snapstate.SnapState + st := s.d.overlord.State() + st.Lock() + err := snapstate.Get(st, "foo", &snapst) + st.Unlock() + c.Assert(err, check.IsNil) + + // modify state + snapst.Channel = "beta" + snapst.IgnoreValidation = true + st.Lock() + snapstate.Set(st, "foo", &snapst) + st.Unlock() + + req, err := http.NewRequest("GET", "/v2/snaps/foo", nil) + c.Assert(err, check.IsNil) + rsp, ok := getSnapInfo(snapCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + + c.Assert(rsp, check.NotNil) + c.Assert(rsp.Result, check.FitsTypeOf, &client.Snap{}) + m := rsp.Result.(*client.Snap) + + // installed-size depends on vagaries of the filesystem, just check type + c.Check(m.InstalledSize, check.FitsTypeOf, int64(0)) + m.InstalledSize = 0 + // ditto install-date + c.Check(m.InstallDate, check.FitsTypeOf, time.Time{}) + m.InstallDate = time.Time{} + + meta := &Meta{} + expected := &resp{ + Type: ResponseTypeSync, + Status: 200, + Result: &client.Snap{ + ID: "foo-id", + Name: "foo", + Revision: snap.R(10), + Version: "v1", + Channel: "stable", + TrackingChannel: "beta", + IgnoreValidation: true, + Title: "title", + Summary: "summary", + Description: "description", + Developer: "bar", + Status: "active", + Icon: "/v2/icons/foo/icon", + Type: string(snap.TypeApp), + Private: false, + DevMode: false, + JailMode: false, + Confinement: string(snap.StrictConfinement), + TryMode: false, + Apps: []client.AppInfo{ + { + Snap: "foo", Name: "cmd", + DesktopFile: df, + }, { + // no desktop file + Snap: "foo", Name: "cmd2", + }, { + // services + Snap: "foo", Name: "svc1", + Daemon: "simple", + Enabled: true, + Active: false, + }, { + Snap: "foo", Name: "svc2", + Daemon: "forking", + Enabled: false, + Active: true, + }, { + Snap: "foo", Name: "svc3", + Daemon: "oneshot", + Enabled: true, + Active: true, + }, + { + Snap: "foo", Name: "svc4", + Daemon: "notify", + Enabled: false, + Active: false, + }, + }, + Broken: "", + Contact: "", + License: "GPL-3.0", + }, + Meta: meta, + } + + c.Check(rsp.Result, check.DeepEquals, expected.Result) +} + +func (s *apiSuite) TestSnapInfoWithAuth(c *check.C) { + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + req, err := http.NewRequest("GET", "/v2/find/?q=name:gfoo", nil) + c.Assert(err, check.IsNil) + + c.Assert(s.user, check.IsNil) + + _, ok := searchStore(findCmd, req, user).(*resp) + c.Assert(ok, check.Equals, true) + // ensure user was set + c.Assert(s.user, check.DeepEquals, user) +} + +func (s *apiSuite) TestSnapInfoNotFound(c *check.C) { + req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) + c.Assert(err, check.IsNil) + c.Check(getSnapInfo(snapCmd, req, nil).(*resp).Status, check.Equals, 404) +} + +func (s *apiSuite) TestSnapInfoNoneFound(c *check.C) { + s.vars = map[string]string{"name": "foo"} + + req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) + c.Assert(err, check.IsNil) + c.Check(getSnapInfo(snapCmd, req, nil).(*resp).Status, check.Equals, 404) +} + +func (s *apiSuite) TestSnapInfoIgnoresRemoteErrors(c *check.C) { + s.vars = map[string]string{"name": "foo"} + s.err = errors.New("weird") + + req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) + c.Assert(err, check.IsNil) + rsp := getSnapInfo(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 404) + c.Check(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestListIncludesAll(c *check.C) { + // Very basic check to help stop us from not adding all the + // commands to the command list. + // + // It could get fancier, looking deeper into the AST to see + // exactly what's being defined, but it's probably not worth + // it; this gives us most of the benefits of that, with a + // fraction of the work. + // + // NOTE: there's probably a + // better/easier way of doing this (patches welcome) + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "api.go", nil, 0) + if err != nil { + panic(err) + } + + found := 0 + + ast.Inspect(f, func(n ast.Node) bool { + switch v := n.(type) { + case *ast.ValueSpec: + found += len(v.Values) + return false + } + return true + }) + + exceptions := []string{ // keep sorted, for scanning ease + "isEmailish", + "api", + "maxReadBuflen", + "muxVars", + "errNothingToInstall", + "errDevJailModeConflict", + "errNoJailMode", + "errClassicDevmodeConflict", + // snapInstruction vars: + "snapInstructionDispTable", + "snapstateInstall", + "snapstateUpdate", + "snapstateInstallPath", + "snapstateTryPath", + "snapstateUpdateMany", + "snapstateInstallMany", + "snapstateRemoveMany", + "snapstateRefreshCandidates", + "snapstateRevert", + "snapstateRevertToRevision", + "snapstateSwitch", + "assertstateRefreshSnapDeclarations", + "unsafeReadSnapInfo", + "osutilAddUser", + "setupLocalUser", + "storeUserInfo", + "postCreateUserUcrednetGet", + "ensureStateSoon", + } + c.Check(found, check.Equals, len(api)+len(exceptions), + check.Commentf(`At a glance it looks like you've not added all the Commands defined in api to the api list. If that is not the case, please add the exception to the "exceptions" list in this test.`)) +} + +func (s *apiSuite) TestRootCmd(c *check.C) { + // check it only does GET + c.Check(rootCmd.PUT, check.IsNil) + c.Check(rootCmd.POST, check.IsNil) + c.Check(rootCmd.DELETE, check.IsNil) + c.Assert(rootCmd.GET, check.NotNil) + + rec := httptest.NewRecorder() + c.Check(rootCmd.Path, check.Equals, "/") + + rootCmd.GET(rootCmd, nil, nil).ServeHTTP(rec, nil) + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json") + + expected := []interface{}{"TBD"} + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *apiSuite) TestSysInfo(c *check.C) { + // check it only does GET + c.Check(sysInfoCmd.PUT, check.IsNil) + c.Check(sysInfoCmd.POST, check.IsNil) + c.Check(sysInfoCmd.DELETE, check.IsNil) + c.Assert(sysInfoCmd.GET, check.NotNil) + + rec := httptest.NewRecorder() + c.Check(sysInfoCmd.Path, check.Equals, "/v2/system-info") + + s.daemon(c).Version = "42b1" + + restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"}) + defer restore() + restore = release.MockOnClassic(true) + defer restore() + restore = release.MockForcedDevmode(true) + defer restore() + + sysInfoCmd.GET(sysInfoCmd, nil, nil).ServeHTTP(rec, nil) + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json") + + expected := map[string]interface{}{ + "series": "16", + "version": "42b1", + "os-release": map[string]interface{}{ + "id": "distro-id", + "version-id": "1.2", + }, + "on-classic": true, + "managed": false, + "locations": map[string]interface{}{ + "snap-mount-dir": dirs.SnapMountDir, + "snap-bin-dir": dirs.SnapBinariesDir, + }, + "refresh": map[string]interface{}{ + "schedule": "00:00-05:59/6:00-11:59/12:00-17:59/18:00-23:59", + }, + "confinement": "partial", + } + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + // Ensure that we had a kernel-verrsion but don't check the actual value. + const kernelVersionKey = "kernel-version" + c.Check(rsp.Result.(map[string]interface{})[kernelVersionKey], check.Not(check.Equals), "") + delete(rsp.Result.(map[string]interface{}), kernelVersionKey) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *apiSuite) makeMyAppsServer(statusCode int, data string) *httptest.Server { + mockMyAppsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + io.WriteString(w, data) + })) + store.MyAppsMacaroonACLAPI = mockMyAppsServer.URL + "/acl/" + return mockMyAppsServer +} + +func (s *apiSuite) makeSSOServer(statusCode int, data string) *httptest.Server { + mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + io.WriteString(w, data) + })) + store.UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge" + return mockSSOServer +} + +func (s *apiSuite) makeStoreMacaroon() (string, error) { + m, err := macaroon.New([]byte("secret"), "some id", "location") + if err != nil { + return "", err + } + err = m.AddFirstPartyCaveat("caveat") + if err != nil { + return "", err + } + err = m.AddThirdPartyCaveat([]byte("shared-secret"), "third-party-caveat", store.UbuntuoneLocation) + if err != nil { + return "", err + } + + return auth.MacaroonSerialize(m) +} + +func (s *apiSuite) makeStoreMacaroonResponse(serializedMacaroon string) (string, error) { + data := map[string]string{ + "macaroon": serializedMacaroon, + } + expectedData, err := json.Marshal(data) + if err != nil { + return "", err + } + + return string(expectedData), nil +} + +func (s *apiSuite) TestLoginUser(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` + mockSSOServer := s.makeSSOServer(200, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(loginCmd, req, nil).(*resp) + + state.Lock() + user, err := auth.User(state, 1) + state.Unlock() + c.Check(err, check.IsNil) + + expected := userResponseData{ + ID: 1, + Email: "email@.com", + + Macaroon: user.Macaroon, + Discharges: user.Discharges, + } + + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + c.Check(user.ID, check.Equals, 1) + c.Check(user.Username, check.Equals, "") + c.Check(user.Email, check.Equals, "email@.com") + c.Check(user.Discharges, check.IsNil) + c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon) + c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) + // snapd macaroon was setup too + snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon) + c.Check(err, check.IsNil) + c.Check(snapdMacaroon.Id(), check.Equals, "1") + c.Check(snapdMacaroon.Location(), check.Equals, "snapd") +} + +func (s *apiSuite) TestLoginUserWithUsername(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` + mockSSOServer := s.makeSSOServer(200, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "username", "email": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(loginCmd, req, nil).(*resp) + + state.Lock() + user, err := auth.User(state, 1) + state.Unlock() + c.Check(err, check.IsNil) + + expected := userResponseData{ + ID: 1, + Username: "username", + Email: "email@.com", + Macaroon: user.Macaroon, + Discharges: user.Discharges, + } + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + c.Check(user.ID, check.Equals, 1) + c.Check(user.Username, check.Equals, "username") + c.Check(user.Email, check.Equals, "email@.com") + c.Check(user.Discharges, check.IsNil) + c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon) + c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) + // snapd macaroon was setup too + snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon) + c.Check(err, check.IsNil) + c.Check(snapdMacaroon.Id(), check.Equals, "1") + c.Check(snapdMacaroon.Location(), check.Equals, "snapd") +} + +func (s *apiSuite) TestLoginUserNoEmailWithExistentLocalUser(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + + // setup local-only user + state.Lock() + localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil) + state.Unlock() + c.Assert(err, check.IsNil) + + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` + mockSSOServer := s.makeSSOServer(200, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "username", "email": "", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon)) + + rsp := loginUser(loginCmd, req, localUser).(*resp) + + expected := userResponseData{ + ID: 1, + Username: "username", + Email: "email@test.com", + + Macaroon: localUser.Macaroon, + Discharges: localUser.Discharges, + } + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + state.Lock() + user, err := auth.User(state, localUser.ID) + state.Unlock() + c.Check(err, check.IsNil) + c.Check(user.Username, check.Equals, "username") + c.Check(user.Email, check.Equals, localUser.Email) + c.Check(user.Macaroon, check.Equals, localUser.Macaroon) + c.Check(user.Discharges, check.IsNil) + c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon) + c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) +} + +func (s *apiSuite) TestLoginUserWithExistentLocalUser(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + + // setup local-only user + state.Lock() + localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil) + state.Unlock() + c.Assert(err, check.IsNil) + + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` + mockSSOServer := s.makeSSOServer(200, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "username", "email": "email@test.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon)) + + rsp := loginUser(loginCmd, req, localUser).(*resp) + + expected := userResponseData{ + ID: 1, + Username: "username", + Email: "email@test.com", + + Macaroon: localUser.Macaroon, + Discharges: localUser.Discharges, + } + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + state.Lock() + user, err := auth.User(state, localUser.ID) + state.Unlock() + c.Check(err, check.IsNil) + c.Check(user.Username, check.Equals, "username") + c.Check(user.Email, check.Equals, localUser.Email) + c.Check(user.Macaroon, check.Equals, localUser.Macaroon) + c.Check(user.Discharges, check.IsNil) + c.Check(user.StoreMacaroon, check.Equals, serializedMacaroon) + c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) +} + +func (s *apiSuite) TestLogoutUser(c *check.C) { + d := s.daemon(c) + state := d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Assert(err, check.IsNil) + + req, err := http.NewRequest("POST", "/v2/logout", nil) + c.Assert(err, check.IsNil) + req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) + + rsp := logoutUser(logoutCmd, req, user).(*resp) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + + state.Lock() + _, err = auth.User(state, user.ID) + state.Unlock() + c.Check(err, check.ErrorMatches, "invalid user") +} + +func (s *apiSuite) TestLoginUserBadRequest(c *check.C) { + buf := bytes.NewBufferString(`hello`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestLoginUserMyAppsError(c *check.C) { + mockMyAppsServer := s.makeMyAppsServer(200, "{}") + defer mockMyAppsServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 401) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "cannot get snap access permission") +} + +func (s *apiSuite) TestLoginUserTwoFactorRequiredError(c *check.C) { + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"code": "TWOFACTOR_REQUIRED"}` + mockSSOServer := s.makeSSOServer(401, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 401) + c.Check(rsp.Result.(*errorResult).Kind, check.Equals, errorKindTwoFactorRequired) +} + +func (s *apiSuite) TestLoginUserTwoFactorFailedError(c *check.C) { + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"code": "TWOFACTOR_FAILURE"}` + mockSSOServer := s.makeSSOServer(403, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 401) + c.Check(rsp.Result.(*errorResult).Kind, check.Equals, errorKindTwoFactorFailed) +} + +func (s *apiSuite) TestLoginUserInvalidCredentialsError(c *check.C) { + serializedMacaroon, err := s.makeStoreMacaroon() + c.Assert(err, check.IsNil) + responseData, err := s.makeStoreMacaroonResponse(serializedMacaroon) + c.Assert(err, check.IsNil) + mockMyAppsServer := s.makeMyAppsServer(200, responseData) + defer mockMyAppsServer.Close() + + discharge := `{"code": "INVALID_CREDENTIALS"}` + mockSSOServer := s.makeSSOServer(401, discharge) + defer mockSSOServer.Close() + + buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + + rsp := loginUser(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 401) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, "invalid credentials") +} + +func (s *apiSuite) TestUserFromRequestNoHeader(c *check.C) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.Equals, auth.ErrInvalidAuth) + c.Check(user, check.IsNil) +} + +func (s *apiSuite) TestUserFromRequestHeaderNoMacaroons(c *check.C) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", "Invalid") + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.ErrorMatches, "authorization header misses Macaroon prefix") + c.Check(user, check.IsNil) +} + +func (s *apiSuite) TestUserFromRequestHeaderIncomplete(c *check.C) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", `Macaroon root=""`) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.ErrorMatches, "invalid authorization header") + c.Check(user, check.IsNil) +} + +func (s *apiSuite) TestUserFromRequestHeaderCorrectMissingUser(c *check.C) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.Equals, auth.ErrInvalidAuth) + c.Check(user, check.IsNil) +} + +func (s *apiSuite) TestUserFromRequestHeaderValidUser(c *check.C) { + state := snapCmd.d.overlord.State() + state.Lock() + expectedUser, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, expectedUser.Macaroon)) + + state.Lock() + user, err := UserFromRequest(state, req) + state.Unlock() + + c.Check(err, check.IsNil) + c.Check(user, check.DeepEquals, expectedUser) +} + +func (s *apiSuite) TestSnapsInfoOnePerIntegration(c *check.C) { + s.checkSnapInfoOnePerIntegration(c, false, nil) +} + +func (s *apiSuite) TestSnapsInfoOnePerIntegrationSome(c *check.C) { + s.checkSnapInfoOnePerIntegration(c, false, []string{"foo", "baz"}) +} + +func (s *apiSuite) TestSnapsInfoOnePerIntegrationAll(c *check.C) { + s.checkSnapInfoOnePerIntegration(c, true, nil) +} + +func (s *apiSuite) TestSnapsInfoOnePerIntegrationAllSome(c *check.C) { + s.checkSnapInfoOnePerIntegration(c, true, []string{"foo", "baz"}) +} + +func (s *apiSuite) checkSnapInfoOnePerIntegration(c *check.C, all bool, names []string) { + d := s.daemon(c) + + type tsnap struct { + name string + dev string + ver string + rev int + active bool + + wanted bool + } + + tsnaps := []tsnap{ + {name: "foo", dev: "bar", ver: "v0.9", rev: 1}, + {name: "foo", dev: "bar", ver: "v1", rev: 5, active: true}, + {name: "bar", dev: "baz", ver: "v2", rev: 10, active: true}, + {name: "baz", dev: "qux", ver: "v3", rev: 15, active: true}, + {name: "qux", dev: "mip", ver: "v4", rev: 20, active: true}, + } + numExpected := 0 + + for _, snp := range tsnaps { + if all || snp.active { + if len(names) == 0 { + numExpected++ + snp.wanted = true + } + for _, n := range names { + if snp.name == n { + numExpected++ + snp.wanted = true + break + } + } + } + s.mkInstalledInState(c, d, snp.name, snp.dev, snp.ver, snap.R(snp.rev), snp.active, "") + } + + q := url.Values{} + if all { + q.Set("select", "all") + } + if len(names) > 0 { + q.Set("snaps", strings.Join(names, ",")) + } + req, err := http.NewRequest("GET", "/v2/snaps?"+q.Encode(), nil) + c.Assert(err, check.IsNil) + + rsp, ok := getSnapsInfo(snapsCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Result, check.NotNil) + + snaps := snapList(rsp.Result) + c.Check(snaps, check.HasLen, numExpected) + + for _, s := range tsnaps { + if !((all || s.active) && s.wanted) { + continue + } + var got map[string]interface{} + for _, got = range snaps { + if got["name"].(string) == s.name && got["revision"].(string) == snap.R(s.rev).String() { + break + } + } + c.Check(got["name"], check.Equals, s.name) + c.Check(got["version"], check.Equals, s.ver) + c.Check(got["revision"], check.Equals, snap.R(s.rev).String()) + c.Check(got["developer"], check.Equals, s.dev) + c.Check(got["confinement"], check.Equals, "strict") + } +} + +func (s *apiSuite) TestSnapsInfoOnlyLocal(c *check.C) { + d := s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps?sources=local", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Assert(rsp.Sources, check.DeepEquals, []string{"local"}) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "local") +} + +func (s *apiSuite) TestSnapsInfoAll(c *check.C) { + d := s.daemon(c) + + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(1), false, "") + s.mkInstalledInState(c, d, "local", "foo", "v2", snap.R(2), false, "") + s.mkInstalledInState(c, d, "local", "foo", "v3", snap.R(3), true, "") + + for _, t := range []struct { + q string + numSnaps int + typ ResponseType + }{ + {"?select=enabled", 1, "sync"}, + {`?select=`, 1, "sync"}, + {"", 1, "sync"}, + {"?select=all", 3, "sync"}, + {"?select=invalid-field", 0, "error"}, + } { + req, err := http.NewRequest("GET", fmt.Sprintf("/v2/snaps%s", t.q), nil) + c.Assert(err, check.IsNil) + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, t.typ) + + if rsp.Type != "error" { + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, t.numSnaps) + c.Assert(snaps[0]["name"], check.Equals, "local") + } + } +} + +func (s *apiSuite) TestFind(c *check.C) { + s.suggestedCurrency = "EUR" + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + + req, err := http.NewRequest("GET", "/v2/find?q=hi", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "store") + c.Check(snaps[0]["prices"], check.IsNil) + c.Check(snaps[0]["screenshots"], check.IsNil) + c.Check(snaps[0]["channels"], check.IsNil) + + c.Check(rsp.SuggestedCurrency, check.Equals, "EUR") + + c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "hi"}) + c.Check(s.refreshCandidates, check.HasLen, 0) +} + +func (s *apiSuite) TestFindRefreshes(c *check.C) { + snapstateRefreshCandidates = snapstate.RefreshCandidates + s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + s.mockSnap(c, "name: store\nversion: 1.0") + + req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "store") + c.Check(s.refreshCandidates, check.HasLen, 1) +} + +func (s *apiSuite) TestFindRefreshSideloaded(c *check.C) { + snapstateRefreshCandidates = snapstate.RefreshCandidates + s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + + s.mockSnap(c, "name: store\nversion: 1.0") + + var snapst snapstate.SnapState + st := s.d.overlord.State() + st.Lock() + err := snapstate.Get(st, "store", &snapst) + st.Unlock() + c.Assert(err, check.IsNil) + c.Assert(snapst.Sequence, check.HasLen, 1) + + // clear the snapid + snapst.Sequence[0].SnapID = "" + st.Lock() + snapstate.Set(st, "store", &snapst) + st.Unlock() + + req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "store") + c.Check(s.refreshCandidates, check.HasLen, 0) +} + +func (s *apiSuite) TestFindPrivate(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{} + + req, err := http.NewRequest("GET", "/v2/find?q=foo&select=private", nil) + c.Assert(err, check.IsNil) + + _ = searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{ + Query: "foo", + Private: true, + }) +} + +func (s *apiSuite) TestFindPrefix(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{} + + req, err := http.NewRequest("GET", "/v2/find?name=foo*", nil) + c.Assert(err, check.IsNil) + + _ = searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo", Prefix: true}) +} + +func (s *apiSuite) TestFindSection(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{} + + req, err := http.NewRequest("GET", "/v2/find?q=foo§ion=bar", nil) + c.Assert(err, check.IsNil) + + _ = searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{ + Query: "foo", + Section: "bar", + }) +} + +func (s *apiSuite) TestFindOne(c *check.C) { + s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + Channels: map[string]*snap.ChannelSnapInfo{ + "stable": { + Revision: snap.R(42), + }, + }, + }} + s.mockSnap(c, "name: store\nversion: 1.0") + + req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{}) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Check(snaps[0]["name"], check.Equals, "store") + m := snaps[0]["channels"].(map[string]interface{})["stable"].(map[string]interface{}) + + c.Check(m["revision"], check.Equals, "42") +} + +func (s *apiSuite) TestFindOneNotFound(c *check.C) { + s.daemon(c) + + s.err = store.ErrSnapNotFound + s.mockSnap(c, "name: store\nversion: 1.0") + + req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{}) + c.Check(rsp.Status, check.Equals, 404) +} + +func (s *apiSuite) TestFindRefreshNotQ(c *check.C) { + req, err := http.NewRequest("GET", "/v2/find?select=refresh&q=foo", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, "cannot use 'q' with 'select=refresh'") +} + +func (s *apiSuite) TestFindPriced(c *check.C) { + s.suggestedCurrency = "GBP" + + s.rsnaps = []*snap.Info{{ + Type: snap.TypeApp, + Version: "v2", + Prices: map[string]float64{ + "GBP": 1.23, + "EUR": 2.34, + }, + MustBuy: true, + SideInfo: snap.SideInfo{ + RealName: "banana", + }, + Publisher: "foo", + }} + + req, err := http.NewRequest("GET", "/v2/find?q=banana&channel=stable", nil) + c.Assert(err, check.IsNil) + rsp, ok := searchStore(findCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + + snap := snaps[0] + c.Check(snap["name"], check.Equals, "banana") + c.Check(snap["prices"], check.DeepEquals, map[string]interface{}{ + "EUR": 2.34, + "GBP": 1.23, + }) + c.Check(snap["status"], check.Equals, "priced") + + c.Check(rsp.SuggestedCurrency, check.Equals, "GBP") +} + +func (s *apiSuite) TestFindScreenshotted(c *check.C) { + s.rsnaps = []*snap.Info{{ + Type: snap.TypeApp, + Version: "v2", + Screenshots: []snap.ScreenshotInfo{ + { + URL: "http://example.com/screenshot.png", + Width: 800, + Height: 1280, + }, + { + URL: "http://example.com/screenshot2.png", + }, + }, + MustBuy: true, + SideInfo: snap.SideInfo{ + RealName: "test-screenshot", + }, + Publisher: "foo", + }} + + req, err := http.NewRequest("GET", "/v2/find?q=test-screenshot", nil) + c.Assert(err, check.IsNil) + rsp, ok := searchStore(findCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + + c.Check(snaps[0]["name"], check.Equals, "test-screenshot") + c.Check(snaps[0]["screenshots"], check.DeepEquals, []interface{}{ + map[string]interface{}{ + "url": "http://example.com/screenshot.png", + "width": float64(800), + "height": float64(1280), + }, + map[string]interface{}{ + "url": "http://example.com/screenshot2.png", + }, + }) +} + +func (s *apiSuite) TestSnapsInfoOnlyStore(c *check.C) { + d := s.daemon(c) + + s.suggestedCurrency = "EUR" + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "store", + }, + Publisher: "foo", + }} + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Assert(rsp.Sources, check.DeepEquals, []string{"store"}) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Assert(snaps[0]["name"], check.Equals, "store") + c.Check(snaps[0]["prices"], check.IsNil) + + c.Check(rsp.SuggestedCurrency, check.Equals, "EUR") +} + +func (s *apiSuite) TestSnapsStoreConfinement(c *check.C) { + s.rsnaps = []*snap.Info{ + { + // no explicit confinement in this one + SideInfo: snap.SideInfo{ + RealName: "foo", + }, + }, + { + Confinement: snap.StrictConfinement, + SideInfo: snap.SideInfo{ + RealName: "bar", + }, + }, + { + Confinement: snap.DevModeConfinement, + SideInfo: snap.SideInfo{ + RealName: "baz", + }, + }, + } + + req, err := http.NewRequest("GET", "/v2/find", nil) + c.Assert(err, check.IsNil) + + rsp := searchStore(findCmd, req, nil).(*resp) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 3) + + for i, ss := range [][2]string{ + {"foo", string(snap.StrictConfinement)}, + {"bar", string(snap.StrictConfinement)}, + {"baz", string(snap.DevModeConfinement)}, + } { + name, mode := ss[0], ss[1] + c.Check(snaps[i]["name"], check.Equals, name, check.Commentf(name)) + c.Check(snaps[i]["confinement"], check.Equals, mode, check.Commentf(name)) + } +} + +func (s *apiSuite) TestSnapsInfoStoreWithAuth(c *check.C) { + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil) + c.Assert(err, check.IsNil) + + c.Assert(s.user, check.IsNil) + + _ = getSnapsInfo(snapsCmd, req, user).(*resp) + + // ensure user was set + c.Assert(s.user, check.DeepEquals, user) +} + +func (s *apiSuite) TestSnapsInfoLocalAndStore(c *check.C) { + d := s.daemon(c) + + s.rsnaps = []*snap.Info{{ + Version: "v42", + SideInfo: snap.SideInfo{ + RealName: "remote", + }, + Publisher: "foo", + }} + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps?sources=local,store", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + // presence of 'store' in sources bounces request over to /find + c.Assert(rsp.Sources, check.DeepEquals, []string{"store"}) + + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Check(snaps[0]["version"], check.Equals, "v42") + + // as does a 'q' + req, err = http.NewRequest("GET", "/v2/snaps?q=what", nil) + c.Assert(err, check.IsNil) + rsp = getSnapsInfo(snapsCmd, req, nil).(*resp) + snaps = snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Check(snaps[0]["version"], check.Equals, "v42") + + // otherwise, local only + req, err = http.NewRequest("GET", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + rsp = getSnapsInfo(snapsCmd, req, nil).(*resp) + snaps = snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) + c.Check(snaps[0]["version"], check.Equals, "v1") +} + +func (s *apiSuite) TestSnapsInfoDefaultSources(c *check.C) { + d := s.daemon(c) + + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "remote", + }, + Publisher: "foo", + }} + s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Assert(rsp.Sources, check.DeepEquals, []string{"local"}) + snaps := snapList(rsp.Result) + c.Assert(snaps, check.HasLen, 1) +} + +func (s *apiSuite) TestSnapsInfoUnknownSource(c *check.C) { + s.rsnaps = []*snap.Info{{ + SideInfo: snap.SideInfo{ + RealName: "remote", + }, + Publisher: "foo", + }} + s.mkInstalled(c, "local", "foo", "v1", snap.R(10), true, "") + + req, err := http.NewRequest("GET", "/v2/snaps?sources=unknown", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Check(rsp.Sources, check.DeepEquals, []string{"local"}) + + snaps := snapList(rsp.Result) + c.Check(snaps, check.HasLen, 1) +} + +func (s *apiSuite) TestSnapsInfoFilterRemote(c *check.C) { + s.rsnaps = nil + + req, err := http.NewRequest("GET", "/v2/snaps?q=foo&sources=store", nil) + c.Assert(err, check.IsNil) + + rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) + + c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo"}) + + c.Assert(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestPostSnapBadRequest(c *check.C) { + buf := bytes.NewBufferString(`hello`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestPostSnapBadAction(c *check.C) { + buf := bytes.NewBufferString(`{"action": "potato"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Result, check.NotNil) +} + +func (s *apiSuite) TestPostSnap(c *check.C) { + d := s.daemonWithOverlordMock(c) + + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + ensureStateSoonImpl(st) + } + + s.vars = map[string]string{"name": "foo"} + + snapInstructionDispTable["install"] = func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) { + return "foooo", nil, nil + } + defer func() { + snapInstructionDispTable["install"] = snapInstall + }() + + buf := bytes.NewBufferString(`{"action": "install"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, "foooo") + var names []string + err = chg.Get("snap-names", &names) + c.Assert(err, check.IsNil) + c.Check(names, check.DeepEquals, []string{"foo"}) + + c.Check(soon, check.Equals, 1) +} + +func (s *apiSuite) TestPostSnapVerfySnapInstruction(c *check.C) { + s.daemonWithOverlordMock(c) + + buf := bytes.NewBufferString(`{"action": "install"}`) + req, err := http.NewRequest("POST", "/v2/snaps/ubuntu-core", buf) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"name": "ubuntu-core"} + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, `cannot install "ubuntu-core", please use "core" instead`) +} + +func (s *apiSuite) TestPostSnapSetsUser(c *check.C) { + d := s.daemon(c) + ensureStateSoon = func(st *state.State) {} + + snapInstructionDispTable["install"] = func(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { + return fmt.Sprintf("", inst.userID), nil, nil + } + defer func() { + snapInstructionDispTable["install"] = snapInstall + }() + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + buf := bytes.NewBufferString(`{"action": "install"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) + + rsp := postSnap(snapCmd, req, user).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, "") +} + +func (s *apiSuite) TestPostSnapDispatch(c *check.C) { + inst := &snapInstruction{Snaps: []string{"foo"}} + + type T struct { + s string + impl snapActionFunc + } + + actions := []T{ + {"install", snapInstall}, + {"refresh", snapUpdate}, + {"remove", snapRemove}, + {"revert", snapRevert}, + {"enable", snapEnable}, + {"disable", snapDisable}, + {"switch", snapSwitch}, + {"xyzzy", nil}, + } + + for _, action := range actions { + inst.Action = action.s + // do you feel dirty yet? + c.Check(fmt.Sprintf("%p", action.impl), check.Equals, fmt.Sprintf("%p", inst.dispatch())) + } +} + +func (s *apiSuite) TestPostSnapEnableDisableSwitchRevision(c *check.C) { + for _, action := range []string{"enable", "disable", "switch"} { + buf := bytes.NewBufferString(`{"action": "` + action + `", "revision": "42"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "takes no revision") + } +} + +var sideLoadBodyWithoutDevMode = "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"dangerous\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap-path\"\r\n" + + "\r\n" + + "a/b/local.snap\r\n" + + "----hello--\r\n" + +func (s *apiSuite) TestSideloadSnapOnNonDevModeDistro(c *check.C) { + // try a multipart/form-data upload + body := sideLoadBodyWithoutDevMode + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + chgSummary := s.sideloadCheck(c, body, head, snapstate.Flags{RemoveSnapPath: true}) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) +} + +func (s *apiSuite) TestSideloadSnapOnDevModeDistro(c *check.C) { + // try a multipart/form-data upload + body := sideLoadBodyWithoutDevMode + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + restore := release.MockForcedDevmode(true) + defer restore() + flags := snapstate.Flags{RemoveSnapPath: true} + chgSummary := s.sideloadCheck(c, body, head, flags) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) +} + +func (s *apiSuite) TestSideloadSnapDevMode(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + // try a multipart/form-data upload + flags := snapstate.Flags{RemoveSnapPath: true} + flags.DevMode = true + chgSummary := s.sideloadCheck(c, body, head, flags) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) +} + +func (s *apiSuite) TestSideloadSnapJailMode(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"jailmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"dangerous\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + // try a multipart/form-data upload + flags := snapstate.Flags{JailMode: true, RemoveSnapPath: true} + chgSummary := s.sideloadCheck(c, body, head, flags) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) +} + +func (s *apiSuite) TestSideloadSnapJailModeAndDevmode(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"jailmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + s.daemonWithOverlordMock(c) + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, "cannot use devmode and jailmode flags together") +} + +func (s *apiSuite) TestSideloadSnapJailModeInDevModeOS(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"jailmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + s.daemonWithOverlordMock(c) + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + restore := release.MockForcedDevmode(true) + defer restore() + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, "this system cannot honour the jailmode flag") +} + +func (s *apiSuite) TestLocalInstallSnapDeriveSideInfo(c *check.C) { + d := s.daemonWithOverlordMock(c) + // add the assertions first + st := d.overlord.State() + assertAdd(st, s.storeSigning.StoreAccountKey("")) + + dev1Acct := assertstest.NewAccount(s.storeSigning, "devel1", nil, "") + assertAdd(st, dev1Acct) + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "x-id", + "snap-name": "x", + "publisher-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + assertAdd(st, snapDecl) + + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ + "snap-sha3-384": "YK0GWATaZf09g_fvspYPqm_qtaiqf-KjaNj5uMEQCjQpuXWPjqQbeBINL5H_A0Lo", + "snap-size": "5", + "snap-id": "x-id", + "snap-revision": "41", + "developer-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + assertAdd(st, snapRev) + + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x.snap\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, channel string, flags snapstate.Flags) (*state.TaskSet, error) { + c.Check(flags, check.Equals, snapstate.Flags{RemoveSnapPath: true}) + c.Check(si, check.DeepEquals, &snap.SideInfo{ + RealName: "x", + SnapID: "x-id", + Revision: snap.R(41), + }) + + return state.NewTaskSet(), nil + } + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, `Install "x" snap from file "x.snap"`) + var names []string + err = chg.Get("snap-names", &names) + c.Assert(err, check.IsNil) + c.Check(names, check.DeepEquals, []string{"x"}) + var apiData map[string]interface{} + err = chg.Get("api-data", &apiData) + c.Assert(err, check.IsNil) + c.Check(apiData, check.DeepEquals, map[string]interface{}{ + "snap-name": "x", + }) +} + +func (s *apiSuite) TestSideloadSnapNoSignaturesDangerOff(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + s.daemonWithOverlordMock(c) + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + // this is the prefix used for tempfiles for sideloading + glob := filepath.Join(os.TempDir(), "snapd-sideload-pkg-*") + glbBefore, _ := filepath.Glob(glob) + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, `cannot find signatures with metadata for snap "x"`) + glbAfter, _ := filepath.Glob(glob) + c.Check(len(glbBefore), check.Equals, len(glbAfter)) +} + +func (s *apiSuite) TestSideloadSnapNotValidFormFile(c *check.C) { + newTestDaemon(c) + + // try a multipart/form-data upload with missing "name" + content := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + + buf := bytes.NewBufferString(content) + req, err := http.NewRequest("POST", "/v2/snaps", buf) + c.Assert(err, check.IsNil) + for k, v := range head { + req.Header.Set(k, v) + } + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Assert(rsp.Result.(*errorResult).Message, check.Matches, `cannot find "snap" file field in provided multipart/form-data payload`) +} + +func (s *apiSuite) TestTrySnap(c *check.C) { + d := s.daemonWithFakeSnapManager(c) + + var err error + + // mock a try dir + tryDir := c.MkDir() + snapYaml := filepath.Join(tryDir, "meta", "snap.yaml") + err = os.MkdirAll(filepath.Dir(snapYaml), 0755) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(snapYaml, []byte("name: foo\nversion: 1.0\n"), 0644) + c.Assert(err, check.IsNil) + + reqForFlags := func(f snapstate.Flags) *http.Request { + b := "" + + "--hello\r\n" + + "Content-Disposition: form-data; name=\"action\"\r\n" + + "\r\n" + + "try\r\n" + + "--hello\r\n" + + "Content-Disposition: form-data; name=\"snap-path\"\r\n" + + "\r\n" + + tryDir + "\r\n" + + "--hello" + + snip := "\r\n" + + "Content-Disposition: form-data; name=%q\r\n" + + "\r\n" + + "true\r\n" + + "--hello" + + if f.DevMode { + b += fmt.Sprintf(snip, "devmode") + } + if f.JailMode { + b += fmt.Sprintf(snip, "jailmode") + } + if f.Classic { + b += fmt.Sprintf(snip, "classic") + } + b += "--\r\n" + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(b)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=hello") + + return req + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + + for _, t := range []struct { + flags snapstate.Flags + desc string + }{ + {snapstate.Flags{}, "core; -"}, + {snapstate.Flags{DevMode: true}, "core; devmode"}, + {snapstate.Flags{JailMode: true}, "core; jailmode"}, + {snapstate.Flags{Classic: true}, "core; classic"}, + } { + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + ensureStateSoonImpl(st) + } + + tryWasCalled := true + snapstateTryPath = func(s *state.State, name, path string, flags snapstate.Flags) (*state.TaskSet, error) { + c.Check(flags, check.DeepEquals, t.flags, check.Commentf(t.desc)) + tryWasCalled = true + t := s.NewTask("fake-install-snap", "Doing a fake try") + return state.NewTaskSet(t), nil + } + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + if name != "core" { + c.Check(flags, check.DeepEquals, t.flags, check.Commentf(t.desc)) + } + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + // try the snap (without an installed core) + st.Unlock() + rsp := postSnaps(snapsCmd, reqForFlags(t.flags), nil).(*resp) + st.Lock() + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync, check.Commentf(t.desc)) + c.Assert(tryWasCalled, check.Equals, true, check.Commentf(t.desc)) + + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil, check.Commentf(t.desc)) + + c.Assert(chg.Tasks(), check.HasLen, 1, check.Commentf(t.desc)) + + st.Unlock() + s.waitTrivialChange(c, chg) + st.Lock() + + c.Check(chg.Kind(), check.Equals, "try-snap", check.Commentf(t.desc)) + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Try "%s" snap from %s`, "foo", tryDir), check.Commentf(t.desc)) + var names []string + err = chg.Get("snap-names", &names) + c.Assert(err, check.IsNil, check.Commentf(t.desc)) + c.Check(names, check.DeepEquals, []string{"foo"}, check.Commentf(t.desc)) + var apiData map[string]interface{} + err = chg.Get("api-data", &apiData) + c.Assert(err, check.IsNil, check.Commentf(t.desc)) + c.Check(apiData, check.DeepEquals, map[string]interface{}{ + "snap-name": "foo", + }, check.Commentf(t.desc)) + + c.Check(soon, check.Equals, 1, check.Commentf(t.desc)) + } +} + +func (s *apiSuite) TestTrySnapRelative(c *check.C) { + req, err := http.NewRequest("POST", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + + rsp := trySnap(snapsCmd, req, nil, "relative-path", snapstate.Flags{}).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "need an absolute path") +} + +func (s *apiSuite) TestTrySnapNotDir(c *check.C) { + req, err := http.NewRequest("POST", "/v2/snaps", nil) + c.Assert(err, check.IsNil) + + rsp := trySnap(snapsCmd, req, nil, "/does/not/exist", snapstate.Flags{}).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "not a snap directory") +} + +func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedFlags snapstate.Flags) string { + d := s.daemonWithFakeSnapManager(c) + + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + ensureStateSoonImpl(st) + } + + // setup done + installQueue := []string{} + unsafeReadSnapInfo = func(path string) (*snap.Info, error) { + return &snap.Info{SuggestedName: "local"}, nil + } + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + // NOTE: ubuntu-core is not installed in developer mode + c.Check(flags, check.Equals, snapstate.Flags{}) + installQueue = append(installQueue, name) + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, channel string, flags snapstate.Flags) (*state.TaskSet, error) { + c.Check(flags, check.DeepEquals, expectedFlags) + + bs, err := ioutil.ReadFile(path) + c.Check(err, check.IsNil) + c.Check(string(bs), check.Equals, "xyzzy") + + installQueue = append(installQueue, si.RealName+"::"+path) + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + buf := bytes.NewBufferString(content) + req, err := http.NewRequest("POST", "/v2/snaps", buf) + c.Assert(err, check.IsNil) + for k, v := range head { + req.Header.Set(k, v) + } + + rsp := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + n := 1 + c.Assert(installQueue, check.HasLen, n) + c.Check(installQueue[n-1], check.Matches, "local::.*/snapd-sideload-pkg-.*") + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(soon, check.Equals, 1) + + c.Assert(chg.Tasks(), check.HasLen, n) + + st.Unlock() + s.waitTrivialChange(c, chg) + st.Lock() + + c.Check(chg.Kind(), check.Equals, "install-snap") + var names []string + err = chg.Get("snap-names", &names) + c.Assert(err, check.IsNil) + c.Check(names, check.DeepEquals, []string{"local"}) + var apiData map[string]interface{} + err = chg.Get("api-data", &apiData) + c.Assert(err, check.IsNil) + c.Check(apiData, check.DeepEquals, map[string]interface{}{ + "snap-name": "local", + }) + + return chg.Summary() +} + +func (s *apiSuite) runGetConf(c *check.C, keys []string, statusCode int) map[string]interface{} { + s.vars = map[string]string{"name": "test-snap"} + req, err := http.NewRequest("GET", "/v2/snaps/test-snap/conf?keys="+strings.Join(keys, ","), nil) + c.Check(err, check.IsNil) + rec := httptest.NewRecorder() + snapConfCmd.GET(snapConfCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, statusCode) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + return body["result"].(map[string]interface{}) +} + +func (s *apiSuite) TestGetConfSingleKey(c *check.C) { + d := s.daemon(c) + + // Set a config that we'll get in a moment + d.overlord.State().Lock() + tr := config.NewTransaction(d.overlord.State()) + tr.Set("test-snap", "test-key1", "test-value1") + tr.Set("test-snap", "test-key2", "test-value2") + tr.Commit() + d.overlord.State().Unlock() + + result := s.runGetConf(c, []string{"test-key1"}, 200) + c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"}) + + result = s.runGetConf(c, []string{"test-key1", "test-key2"}, 200) + c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1", "test-key2": "test-value2"}) +} + +func (s *apiSuite) TestGetConfMissingKey(c *check.C) { + result := s.runGetConf(c, []string{"test-key2"}, 400) + c.Check(result, check.DeepEquals, map[string]interface{}{"message": `snap "test-snap" has no "test-key2" configuration option`}) +} + +func (s *apiSuite) TestGetRootDocument(c *check.C) { + d := s.daemon(c) + d.overlord.State().Lock() + tr := config.NewTransaction(d.overlord.State()) + tr.Set("test-snap", "test-key1", "test-value1") + tr.Set("test-snap", "test-key2", "test-value2") + tr.Commit() + d.overlord.State().Unlock() + + result := s.runGetConf(c, nil, 200) + c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1", "test-key2": "test-value2"}) +} + +func (s *apiSuite) TestGetConfBadKey(c *check.C) { + // TODO: this one in particular should really be a 400 also + result := s.runGetConf(c, []string{"."}, 500) + c.Check(result, check.DeepEquals, map[string]interface{}{"message": `invalid option name: ""`}) +} + +func (s *apiSuite) TestSetConf(c *check.C) { + d := s.daemon(c) + s.mockSnap(c, configYaml) + + // Mock the hook runner + hookRunner := testutil.MockCommand(c, "snap", "") + defer hookRunner.Restore() + + d.overlord.Loop() + defer d.overlord.Stop() + + text, err := json.Marshal(map[string]interface{}{"key": "value"}) + c.Assert(err, check.IsNil) + + buffer := bytes.NewBuffer(text) + req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "config-snap"} + + rec := httptest.NewRecorder() + snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Assert(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + // Check that the configure hook was run correctly + c.Check(hookRunner.Calls(), check.DeepEquals, [][]string{{ + "snap", "run", "--hook", "configure", "-r", "unset", "config-snap", + }}) +} + +func (s *apiSuite) TestSetConfNumber(c *check.C) { + d := s.daemon(c) + s.mockSnap(c, configYaml) + + // Mock the hook runner + hookRunner := testutil.MockCommand(c, "snap", "") + defer hookRunner.Restore() + + d.overlord.Loop() + defer d.overlord.Stop() + + text, err := json.Marshal(map[string]interface{}{"key": 1234567890}) + c.Assert(err, check.IsNil) + + buffer := bytes.NewBuffer(text) + req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "config-snap"} + + rec := httptest.NewRecorder() + snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Assert(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + defer st.Unlock() + tr := config.NewTransaction(d.overlord.State()) + var result interface{} + c.Assert(tr.Get("config-snap", "key", &result), check.IsNil) + c.Assert(result, check.DeepEquals, json.Number("1234567890")) +} + +func (s *apiSuite) TestSetConfBadSnap(c *check.C) { + s.daemonWithOverlordMock(c) + + text, err := json.Marshal(map[string]interface{}{"key": "value"}) + c.Assert(err, check.IsNil) + + buffer := bytes.NewBuffer(text) + req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "config-snap"} + + rec := httptest.NewRecorder() + snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 404) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Assert(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "status-code": 404., + "status": "Not Found", + "result": map[string]interface{}{ + "message": `snap "config-snap" is not installed`, + "kind": "snap-not-found", + "value": "config-snap", + }, + "type": "error"}) +} + +func (s *apiSuite) TestAppIconGet(c *check.C) { + d := s.daemon(c) + + // have an active foo in the system + info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "") + + // have an icon for it in the package itself + iconfile := filepath.Join(info.MountDir(), "meta", "gui", "icon.ick") + c.Assert(os.MkdirAll(filepath.Dir(iconfile), 0755), check.IsNil) + c.Check(ioutil.WriteFile(iconfile, []byte("ick"), 0644), check.IsNil) + + s.vars = map[string]string{"name": "foo"} + req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + + appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.Body.String(), check.Equals, "ick") +} + +func (s *apiSuite) TestAppIconGetInactive(c *check.C) { + d := s.daemon(c) + + // have an *in*active foo in the system + info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), false, "") + + // have an icon for it in the package itself + iconfile := filepath.Join(info.MountDir(), "meta", "gui", "icon.ick") + c.Assert(os.MkdirAll(filepath.Dir(iconfile), 0755), check.IsNil) + c.Check(ioutil.WriteFile(iconfile, []byte("ick"), 0644), check.IsNil) + + s.vars = map[string]string{"name": "foo"} + req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + + appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.Body.String(), check.Equals, "ick") +} + +func (s *apiSuite) TestAppIconGetNoIcon(c *check.C) { + d := s.daemon(c) + + // have an *in*active foo in the system + info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "") + + // NO ICON! + err := os.RemoveAll(filepath.Join(info.MountDir(), "meta", "gui", "icon.svg")) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "foo"} + req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + + appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code/100, check.Equals, 4) +} + +func (s *apiSuite) TestAppIconGetNoApp(c *check.C) { + s.daemon(c) + + s.vars = map[string]string{"name": "foo"} + req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + + appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 404) +} + +func (s *apiSuite) TestNotInstalledSnapIcon(c *check.C) { + info := &snap.Info{SuggestedName: "notInstalledSnap", IconURL: "icon.svg"} + iconfile := snapIcon(info) + c.Check(iconfile, testutil.Contains, "icon.svg") +} + +func (s *apiSuite) TestInstallOnNonDevModeDistro(c *check.C) { + s.testInstall(c, false, snapstate.Flags{}, snap.R(0)) +} +func (s *apiSuite) TestInstallOnDevModeDistro(c *check.C) { + s.testInstall(c, true, snapstate.Flags{}, snap.R(0)) +} +func (s *apiSuite) TestInstallRevision(c *check.C) { + s.testInstall(c, false, snapstate.Flags{}, snap.R(42)) +} + +func (s *apiSuite) testInstall(c *check.C, forcedDevmode bool, flags snapstate.Flags, revision snap.Revision) { + calledFlags := snapstate.Flags{} + installQueue := []string{} + restore := release.MockForcedDevmode(forcedDevmode) + defer restore() + + snapstateInstall = func(s *state.State, name, channel string, revno snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + installQueue = append(installQueue, name) + c.Check(revision, check.Equals, revno) + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + defer func() { + snapstateInstall = nil + }() + + d := s.daemonWithFakeSnapManager(c) + + var buf bytes.Buffer + if revision.Unset() { + buf.WriteString(`{"action": "install"}`) + } else { + fmt.Fprintf(&buf, `{"action": "install", "revision": %s}`, revision.String()) + } + req, err := http.NewRequest("POST", "/v2/snaps/some-snap", &buf) + c.Assert(err, check.IsNil) + + s.vars = map[string]string{"name": "some-snap"} + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(chg.Tasks(), check.HasLen, 1) + + st.Unlock() + s.waitTrivialChange(c, chg) + st.Lock() + + c.Check(chg.Status(), check.Equals, state.DoneStatus) + c.Check(calledFlags, check.Equals, flags) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(chg.Kind(), check.Equals, "install-snap") + c.Check(chg.Summary(), check.Equals, `Install "some-snap" snap`) +} + +func (s *apiSuite) TestRefresh(c *check.C) { + var calledFlags snapstate.Flags + calledUserID := 0 + installQueue := []string{} + assertstateCalledUserID := 0 + + snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + calledUserID = userID + installQueue = append(installQueue, name) + + t := s.NewTask("fake-refresh-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + assertstateCalledUserID = userID + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + Snaps: []string{"some-snap"}, + userID: 17, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + summary, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(assertstateCalledUserID, check.Equals, 17) + c.Check(calledFlags, check.DeepEquals, snapstate.Flags{}) + c.Check(calledUserID, check.Equals, 17) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(summary, check.Equals, `Refresh "some-snap" snap`) +} + +func (s *apiSuite) TestRefreshDevMode(c *check.C) { + var calledFlags snapstate.Flags + calledUserID := 0 + installQueue := []string{} + + snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + calledUserID = userID + installQueue = append(installQueue, name) + + t := s.NewTask("fake-refresh-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + DevMode: true, + Snaps: []string{"some-snap"}, + userID: 17, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + summary, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + flags := snapstate.Flags{} + flags.DevMode = true + c.Check(calledFlags, check.DeepEquals, flags) + c.Check(calledUserID, check.Equals, 17) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(summary, check.Equals, `Refresh "some-snap" snap`) +} + +func (s *apiSuite) TestRefreshClassic(c *check.C) { + var calledFlags snapstate.Flags + + snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + return nil, nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + Classic: true, + Snaps: []string{"some-snap"}, + userID: 17, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(calledFlags, check.DeepEquals, snapstate.Flags{Classic: true}) +} + +func (s *apiSuite) TestRefreshIgnoreValidation(c *check.C) { + var calledFlags snapstate.Flags + calledUserID := 0 + installQueue := []string{} + + snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + calledUserID = userID + installQueue = append(installQueue, name) + + t := s.NewTask("fake-refresh-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + IgnoreValidation: true, + Snaps: []string{"some-snap"}, + userID: 17, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + summary, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + flags := snapstate.Flags{} + flags.IgnoreValidation = true + + c.Check(calledFlags, check.DeepEquals, flags) + c.Check(calledUserID, check.Equals, 17) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(summary, check.Equals, `Refresh "some-snap" snap`) +} + +func (s *apiSuite) TestPostSnapsOp(c *check.C) { + assertstateRefreshSnapDeclarations = func(*state.State, int) error { return nil } + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 0) + t := s.NewTask("fake-refresh-all", "Refreshing everything") + return []string{"fake1", "fake2"}, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemonWithOverlordMock(c) + + buf := bytes.NewBufferString(`{"action": "refresh"}`) + req, err := http.NewRequest("POST", "/v2/login", buf) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "application/json") + + rsp, ok := postSnaps(snapsCmd, req, nil).(*resp) + c.Assert(ok, check.Equals, true) + c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Check(chg.Summary(), check.Equals, `Refresh snaps "fake1", "fake2"`) + var apiData map[string]interface{} + c.Check(chg.Get("api-data", &apiData), check.IsNil) + c.Check(apiData["snap-names"], check.DeepEquals, []interface{}{"fake1", "fake2"}) +} + +func (s *apiSuite) TestRefreshAll(c *check.C) { + refreshSnapDecls := false + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + refreshSnapDecls = true + return assertstate.RefreshSnapDeclarations(s, userID) + } + d := s.daemon(c) + + for _, tst := range []struct { + snaps []string + msg string + }{ + {nil, "Refresh all snaps: no updates"}, + {[]string{"fake"}, `Refresh snap "fake"`}, + {[]string{"fake1", "fake2"}, `Refresh snaps "fake1", "fake2"`}, + } { + refreshSnapDecls = false + + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 0) + t := s.NewTask("fake-refresh-all", "Refreshing everything") + return tst.snaps, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + inst := &snapInstruction{Action: "refresh"} + st := d.overlord.State() + st.Lock() + summary, _, _, err := snapUpdateMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, tst.msg) + c.Check(refreshSnapDecls, check.Equals, true) + } +} + +func (s *apiSuite) TestRefreshAllNoChanges(c *check.C) { + refreshSnapDecls := false + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + refreshSnapDecls = true + return assertstate.RefreshSnapDeclarations(s, userID) + } + + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 0) + return nil, nil, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "refresh"} + st := d.overlord.State() + st.Lock() + summary, _, _, err := snapUpdateMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Refresh all snaps: no updates`) + c.Check(refreshSnapDecls, check.Equals, true) +} + +func (s *apiSuite) TestRefreshMany(c *check.C) { + refreshSnapDecls := false + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + refreshSnapDecls = true + return nil + } + + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 2) + t := s.NewTask("fake-refresh-2", "Refreshing two") + return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "refresh", Snaps: []string{"foo", "bar"}} + st := d.overlord.State() + st.Lock() + summary, updates, _, err := snapUpdateMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Refresh snaps "foo", "bar"`) + c.Check(updates, check.DeepEquals, inst.Snaps) + c.Check(refreshSnapDecls, check.Equals, true) +} + +func (s *apiSuite) TestRefreshMany1(c *check.C) { + refreshSnapDecls := false + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + refreshSnapDecls = true + return nil + } + + snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 1) + t := s.NewTask("fake-refresh-1", "Refreshing one") + return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "refresh", Snaps: []string{"foo"}} + st := d.overlord.State() + st.Lock() + summary, updates, _, err := snapUpdateMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Refresh snap "foo"`) + c.Check(updates, check.DeepEquals, inst.Snaps) + c.Check(refreshSnapDecls, check.Equals, true) +} + +func (s *apiSuite) TestInstallMany(c *check.C) { + snapstateInstallMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 2) + t := s.NewTask("fake-install-2", "Install two") + return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "install", Snaps: []string{"foo", "bar"}} + st := d.overlord.State() + st.Lock() + summary, installs, _, err := snapInstallMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Install snaps "foo", "bar"`) + c.Check(installs, check.DeepEquals, inst.Snaps) +} + +func (s *apiSuite) TestRemoveMany(c *check.C) { + snapstateRemoveMany = func(s *state.State, names []string) ([]string, []*state.TaskSet, error) { + c.Check(names, check.HasLen, 2) + t := s.NewTask("fake-remove-2", "Remove two") + return names, []*state.TaskSet{state.NewTaskSet(t)}, nil + } + + d := s.daemon(c) + inst := &snapInstruction{Action: "remove", Snaps: []string{"foo", "bar"}} + st := d.overlord.State() + st.Lock() + summary, removes, _, err := snapRemoveMany(inst, st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Check(summary, check.Equals, `Remove snaps "foo", "bar"`) + c.Check(removes, check.DeepEquals, inst.Snaps) +} + +func (s *apiSuite) TestInstallFails(c *check.C) { + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + t := s.NewTask("fake-install-snap-error", "Install task") + return state.NewTaskSet(t), nil + } + + d := s.daemonWithFakeSnapManager(c) + + buf := bytes.NewBufferString(`{"action": "install"}`) + req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) + c.Assert(err, check.IsNil) + + rsp := postSnap(snapCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + + c.Check(chg.Tasks(), check.HasLen, 1) + + st.Unlock() + s.waitTrivialChange(c, chg) + st.Lock() + + c.Check(chg.Err(), check.ErrorMatches, `(?sm).*Install task \(fake-install-snap-error errored\)`) +} + +func (s *apiSuite) TestInstallLeaveOld(c *check.C) { + c.Skip("temporarily dropped half-baked support while sorting out flag mess") + var calledFlags snapstate.Flags + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + LeaveOld: true, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Assert(err, check.IsNil) + + c.Check(calledFlags, check.DeepEquals, snapstate.Flags{}) + c.Check(err, check.IsNil) +} + +func (s *apiSuite) TestInstallDevMode(c *check.C) { + var calledFlags snapstate.Flags + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + // Install the snap in developer mode + DevMode: true, + Snaps: []string{"fake"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(calledFlags.DevMode, check.Equals, true) +} + +func (s *apiSuite) TestInstallJailMode(c *check.C) { + var calledFlags snapstate.Flags + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + JailMode: true, + Snaps: []string{"fake"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(calledFlags.JailMode, check.Equals, true) +} + +func (s *apiSuite) TestInstallJailModeDevModeOS(c *check.C) { + restore := release.MockForcedDevmode(true) + defer restore() + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + JailMode: true, + Snaps: []string{"foo"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.ErrorMatches, "this system cannot honour the jailmode flag") +} + +func (s *apiSuite) TestInstallJailModeDevMode(c *check.C) { + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + DevMode: true, + JailMode: true, + Snaps: []string{"foo"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.ErrorMatches, "cannot use devmode and jailmode flags together") +} + +func (s *apiSuite) testRevertSnap(inst *snapInstruction, c *check.C) { + queue := []string{} + + instFlags, err := inst.modeFlags() + c.Assert(err, check.IsNil) + + snapstateRevert = func(s *state.State, name string, flags snapstate.Flags) (*state.TaskSet, error) { + c.Check(flags, check.Equals, instFlags) + queue = append(queue, name) + return nil, nil + } + snapstateRevertToRevision = func(s *state.State, name string, rev snap.Revision, flags snapstate.Flags) (*state.TaskSet, error) { + c.Check(flags, check.Equals, instFlags) + queue = append(queue, fmt.Sprintf("%s (%s)", name, rev)) + return nil, nil + } + + d := s.daemon(c) + inst.Action = "revert" + inst.Snaps = []string{"some-snap"} + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + summary, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + if inst.Revision.Unset() { + c.Check(queue, check.DeepEquals, []string{inst.Snaps[0]}) + } else { + c.Check(queue, check.DeepEquals, []string{fmt.Sprintf("%s (%s)", inst.Snaps[0], inst.Revision)}) + } + c.Check(summary, check.Equals, `Revert "some-snap" snap`) +} + +func (s *apiSuite) TestRevertSnap(c *check.C) { + s.testRevertSnap(&snapInstruction{}, c) +} + +func (s *apiSuite) TestRevertSnapDevMode(c *check.C) { + s.testRevertSnap(&snapInstruction{DevMode: true}, c) +} + +func (s *apiSuite) TestRevertSnapJailMode(c *check.C) { + s.testRevertSnap(&snapInstruction{JailMode: true}, c) +} + +func (s *apiSuite) TestRevertSnapClassic(c *check.C) { + s.testRevertSnap(&snapInstruction{Classic: true}, c) +} + +func (s *apiSuite) TestRevertSnapToRevision(c *check.C) { + s.testRevertSnap(&snapInstruction{Revision: snap.R(1)}, c) +} + +func (s *apiSuite) TestRevertSnapToRevisionDevMode(c *check.C) { + s.testRevertSnap(&snapInstruction{Revision: snap.R(1), DevMode: true}, c) +} + +func (s *apiSuite) TestRevertSnapToRevisionJailMode(c *check.C) { + s.testRevertSnap(&snapInstruction{Revision: snap.R(1), JailMode: true}, c) +} + +func (s *apiSuite) TestRevertSnapToRevisionClassic(c *check.C) { + s.testRevertSnap(&snapInstruction{Revision: snap.R(1), Classic: true}, c) +} + +func snapList(rawSnaps interface{}) []map[string]interface{} { + snaps := make([]map[string]interface{}, len(rawSnaps.([]*json.RawMessage))) + for i, raw := range rawSnaps.([]*json.RawMessage) { + err := json.Unmarshal([]byte(*raw), &snaps[i]) + if err != nil { + panic(err) + } + } + return snaps +} + +// Tests for GET /v2/interfaces + +func (s *apiSuite) TestInterfaces(c *check.C) { + builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + repo := d.overlord.InterfaceManager().Repository() + connRef := interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + c.Assert(repo.Connect(connRef), check.IsNil) + + req, err := http.NewRequest("GET", "/v2/interfaces", nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.GET(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +/** +// Tests for GET /v2/interface (note: singular!) + +func (s *apiSuite) TestInterfaceIndex(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{ + InterfaceName: "test", + InterfaceStaticInfo: interfaces.StaticInfo{ + Summary: "summary", + }, + }) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + repo := d.overlord.InterfaceManager().Repository() + connRef := interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + c.Assert(repo.Connect(connRef), check.IsNil) + + req, err := http.NewRequest("GET", "/v2/interface", nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfaceIndexCmd.GET(interfaceIndexCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + // The body contains large number of interface names, ensure that just the + // test one, added above, exists. + c.Check(body["result"], testutil.DeepContains, map[string]interface{}{ + "name": "test", + "summary": "summary", + "used": true, + }) + c.Check(body["status"], check.Equals, "OK") + c.Check(body["status-code"], check.Equals, 200.0) + c.Check(body["type"], check.Equals, "sync") +} + +// Tests for GET /v2/interface/test + +func (s *apiSuite) TestInterfaceDetail(c *check.C) { + _ = s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{ + InterfaceName: "test", + InterfaceStaticInfo: interfaces.StaticInfo{ + Summary: "summary", + }, + }) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + // NOTE: this is confusing, we must set s.vars manually, + s.vars = map[string]string{"name": "test"} + req, err := http.NewRequest("GET", "/v2/interface/test", nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfaceDetailCmd.GET(interfaceDetailCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "name": "test", + "summary": "summary", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "label": "label", + "attrs": map[string]interface{}{"key": "value"}, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "label": "label", + "attrs": map[string]interface{}{"key": "value"}, + }, + }, + "used": true, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestInterfaceDetail404(c *check.C) { + _ = s.daemon(c) + + // NOTE: this is confusing, we must set s.vars manually, + s.vars = map[string]string{"name": "test"} + req, err := http.NewRequest("GET", "/v2/interface/test", nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfaceDetailCmd.GET(interfaceDetailCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 404) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": `cannot find interface named "test"`, + }, + "status": "Not Found", + "status-code": 404.0, + "type": "error", + }) +} + +**/ + +// Test for POST /v2/interfaces + +func (s *apiSuite) TestConnectPlugSuccess(c *check.C) { + builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "connect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + repo := d.overlord.InterfaceManager().Repository() + ifaces := repo.Interfaces() + c.Assert(ifaces.Connections, check.HasLen, 1) + c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{interfaces.PlugRef{Snap: "consumer", Name: "plug"}, interfaces.SlotRef{Snap: "producer", Name: "slot"}}}) +} + +func (s *apiSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "different"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, differentProducerYaml) + + action := &interfaceAction{ + Action: "connect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) + repo := d.overlord.InterfaceManager().Repository() + ifaces := repo.Interfaces() + c.Assert(ifaces.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + // there is no consumer, no plug defined + s.mockSnap(c, producerYaml) + s.mockSnap(c, consumerYaml) + + action := &interfaceAction{ + Action: "connect", + Plugs: []plugJSON{{Snap: "consumer", Name: "missingplug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "snap \"consumer\" has no plug named \"missingplug\"", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) + + repo := d.overlord.InterfaceManager().Repository() + ifaces := repo.Interfaces() + c.Assert(ifaces.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + // there is no producer, no slot defined + + action := &interfaceAction{ + Action: "connect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "missingslot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "snap \"producer\" has no slot named \"missingslot\"", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) + + repo := d.overlord.InterfaceManager().Repository() + ifaces := repo.Interfaces() + c.Assert(ifaces.Connections, check.HasLen, 0) +} + +func (s *apiSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) { + builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + repo := d.overlord.InterfaceManager().Repository() + connRef := interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + c.Assert(repo.Connect(connRef), check.IsNil) + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &interfaceAction{ + Action: "disconnect", + Plugs: []plugJSON{{Snap: plugSnap, Name: plugName}}, + Slots: []slotJSON{{Snap: slotSnap, Name: slotName}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + ifaces := repo.Interfaces() + c.Assert(ifaces.Connections, check.HasLen, 0) +} + +func (s *apiSuite) TestDisconnectPlugSuccess(c *check.C) { + s.testDisconnect(c, "consumer", "plug", "producer", "slot") +} + +func (s *apiSuite) TestDisconnectPlugSuccessWithEmptyPlug(c *check.C) { + s.testDisconnect(c, "", "", "producer", "slot") +} + +func (s *apiSuite) TestDisconnectPlugSuccessWithEmptySlot(c *check.C) { + s.testDisconnect(c, "consumer", "plug", "", "") +} + +func (s *apiSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) { + builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + s.daemon(c) + + // there is no consumer, no plug defined + s.mockSnap(c, producerYaml) + + action := &interfaceAction{ + Action: "disconnect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "snap \"consumer\" has no plug named \"plug\"", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) { + builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + s.daemon(c) + + s.mockSnap(c, consumerYaml) + // there is no producer, no slot defined + + action := &interfaceAction{ + Action: "disconnect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "snap \"producer\" has no slot named \"slot\"", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestDisconnectPlugFailureNotConnected(c *check.C) { + builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + action := &interfaceAction{ + Action: "disconnect", + Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, + Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "cannot disconnect consumer:plug from producer:slot, it is not connected", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestUnsupportedInterfaceRequest(c *check.C) { + buf := bytes.NewBuffer([]byte(`garbage`)) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestMissingInterfaceAction(c *check.C) { + action := &interfaceAction{} + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "interface action not specified", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestUnsupportedInterfaceAction(c *check.C) { + s.daemon(c) + action := &interfaceAction{Action: "foo"} + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/interfaces", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "unsupported interface action: \"foo\"", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestGetAsserts(c *check.C) { + s.daemon(c) + resp := assertsCmd.GET(assertsCmd, nil, nil).(*resp) + c.Check(resp.Status, check.Equals, 200) + c.Check(resp.Type, check.Equals, ResponseTypeSync) + c.Check(resp.Result, check.DeepEquals, map[string][]string{"types": asserts.TypeNames()}) +} + +func assertAdd(st *state.State, a asserts.Assertion) { + st.Lock() + defer st.Unlock() + err := assertstate.Add(st, a) + if err != nil { + panic(err) + } +} + +func (s *apiSuite) TestAssertOK(c *check.C) { + // Setup + d := s.daemon(c) + st := d.overlord.State() + // add store key + assertAdd(st, s.storeSigning.StoreAccountKey("")) + + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + buf := bytes.NewBuffer(asserts.Encode(acct)) + // Execute + req, err := http.NewRequest("POST", "/v2/assertions", buf) + c.Assert(err, check.IsNil) + rsp := doAssert(assertsCmd, req, nil).(*resp) + // Verify (external) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + // Verify (internal) + st.Lock() + defer st.Unlock() + _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{ + "account-id": acct.AccountID(), + }) + c.Check(err, check.IsNil) +} + +func (s *apiSuite) TestAssertStreamOK(c *check.C) { + // Setup + d := s.daemon(c) + st := d.overlord.State() + + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + buf := &bytes.Buffer{} + enc := asserts.NewEncoder(buf) + err := enc.Encode(acct) + c.Assert(err, check.IsNil) + err = enc.Encode(s.storeSigning.StoreAccountKey("")) + c.Assert(err, check.IsNil) + + // Execute + req, err := http.NewRequest("POST", "/v2/assertions", buf) + c.Assert(err, check.IsNil) + rsp := doAssert(assertsCmd, req, nil).(*resp) + // Verify (external) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + // Verify (internal) + st.Lock() + defer st.Unlock() + _, err = assertstate.DB(st).Find(asserts.AccountType, map[string]string{ + "account-id": acct.AccountID(), + }) + c.Check(err, check.IsNil) +} + +func (s *apiSuite) TestAssertInvalid(c *check.C) { + // Setup + buf := bytes.NewBufferString("blargh") + req, err := http.NewRequest("POST", "/v2/assertions", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + // Execute + assertsCmd.POST(assertsCmd, req, nil).ServeHTTP(rec, req) + // Verify (external) + c.Check(rec.Code, check.Equals, 400) + c.Check(rec.Body.String(), testutil.Contains, + "cannot decode request body into assertions") +} + +func (s *apiSuite) TestAssertError(c *check.C) { + s.daemon(c) + // Setup + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + buf := bytes.NewBuffer(asserts.Encode(acct)) + req, err := http.NewRequest("POST", "/v2/assertions", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + // Execute + assertsCmd.POST(assertsCmd, req, nil).ServeHTTP(rec, req) + // Verify (external) + c.Check(rec.Code, check.Equals, 400) + c.Check(rec.Body.String(), testutil.Contains, "assert failed") +} + +func (s *apiSuite) TestAssertsFindManyAll(c *check.C) { + // Setup + d := s.daemon(c) + // add store key + st := d.overlord.State() + assertAdd(st, s.storeSigning.StoreAccountKey("")) + acct := assertstest.NewAccount(s.storeSigning, "developer1", map[string]interface{}{ + "account-id": "developer1-id", + }, "") + assertAdd(st, acct) + + // Execute + req, err := http.NewRequest("POST", "/v2/assertions/account", nil) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"assertType": "account"} + rec := httptest.NewRecorder() + assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req) + // Verify + c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body)) + c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/x.ubuntu.assertion; bundle=y") + c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "4") + dec := asserts.NewDecoder(rec.Body) + a1, err := dec.Decode() + c.Assert(err, check.IsNil) + c.Check(a1.Type(), check.Equals, asserts.AccountType) + + a2, err := dec.Decode() + c.Assert(err, check.IsNil) + + a3, err := dec.Decode() + c.Assert(err, check.IsNil) + + a4, err := dec.Decode() + c.Assert(err, check.IsNil) + + _, err = dec.Decode() + c.Assert(err, check.Equals, io.EOF) + + ids := []string{a1.(*asserts.Account).AccountID(), a2.(*asserts.Account).AccountID(), a3.(*asserts.Account).AccountID(), a4.(*asserts.Account).AccountID()} + sort.Strings(ids) + c.Check(ids, check.DeepEquals, []string{"can0nical", "canonical", "developer1-id", "generic"}) +} + +func (s *apiSuite) TestAssertsFindManyFilter(c *check.C) { + // Setup + d := s.daemon(c) + // add store key + st := d.overlord.State() + assertAdd(st, s.storeSigning.StoreAccountKey("")) + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + assertAdd(st, acct) + + // Execute + req, err := http.NewRequest("POST", "/v2/assertions/account?username=developer1", nil) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"assertType": "account"} + rec := httptest.NewRecorder() + assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req) + // Verify + c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body)) + c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "1") + dec := asserts.NewDecoder(rec.Body) + a1, err := dec.Decode() + c.Assert(err, check.IsNil) + c.Check(a1.Type(), check.Equals, asserts.AccountType) + c.Check(a1.(*asserts.Account).Username(), check.Equals, "developer1") + c.Check(a1.(*asserts.Account).AccountID(), check.Equals, acct.AccountID()) + _, err = dec.Decode() + c.Check(err, check.Equals, io.EOF) +} + +func (s *apiSuite) TestAssertsFindManyNoResults(c *check.C) { + // Setup + d := s.daemon(c) + // add store key + st := d.overlord.State() + assertAdd(st, s.storeSigning.StoreAccountKey("")) + acct := assertstest.NewAccount(s.storeSigning, "developer1", nil, "") + assertAdd(st, acct) + + // Execute + req, err := http.NewRequest("POST", "/v2/assertions/account?username=xyzzyx", nil) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"assertType": "account"} + rec := httptest.NewRecorder() + assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req) + // Verify + c.Check(rec.Code, check.Equals, 200, check.Commentf("body %q", rec.Body)) + c.Check(rec.HeaderMap.Get("X-Ubuntu-Assertions-Count"), check.Equals, "0") + dec := asserts.NewDecoder(rec.Body) + _, err = dec.Decode() + c.Check(err, check.Equals, io.EOF) +} + +func (s *apiSuite) TestAssertsInvalidType(c *check.C) { + // Execute + req, err := http.NewRequest("POST", "/v2/assertions/foo", nil) + c.Assert(err, check.IsNil) + s.vars = map[string]string{"assertType": "foo"} + rec := httptest.NewRecorder() + assertsFindManyCmd.GET(assertsFindManyCmd, req, nil).ServeHTTP(rec, req) + // Verify + c.Check(rec.Code, check.Equals, 400) + c.Check(rec.Body.String(), testutil.Contains, "invalid assert type") +} + +func setupChanges(st *state.State) []string { + chg1 := st.NewChange("install", "install...") + chg1.Set("snap-names", []string{"funky-snap-name"}) + t1 := st.NewTask("download", "1...") + t2 := st.NewTask("activate", "2...") + chg1.AddAll(state.NewTaskSet(t1, t2)) + t1.Logf("l11") + t1.Logf("l12") + chg2 := st.NewChange("remove", "remove..") + t3 := st.NewTask("unlink", "1...") + chg2.AddTask(t3) + t3.SetStatus(state.ErrorStatus) + t3.Errorf("rm failed") + + return []string{chg1.ID(), chg2.ID(), t1.ID(), t2.ID(), t3.ID()} +} + +func (s *apiSuite) TestStateChangesDefaultToInProgress(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + c.Assert(rsp.Result, check.HasLen, 1) + + res, err := rsp.MarshalJSON() + c.Assert(err, check.IsNil) + + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*`) +} + +func (s *apiSuite) TestStateChangesInProgress(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes?select=in-progress", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + c.Assert(rsp.Result, check.HasLen, 1) + + res, err := rsp.MarshalJSON() + c.Assert(err, check.IsNil) + + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`) +} + +func (s *apiSuite) TestStateChangesAll(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes?select=all", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Status, check.Equals, 200) + c.Assert(rsp.Result, check.HasLen, 2) + + res, err := rsp.MarshalJSON() + c.Assert(err, check.IsNil) + + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`) + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`) +} + +func (s *apiSuite) TestStateChangesReady(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes?select=ready", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Status, check.Equals, 200) + c.Assert(rsp.Result, check.HasLen, 1) + + res, err := rsp.MarshalJSON() + c.Assert(err, check.IsNil) + + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`) +} + +func (s *apiSuite) TestStateChangesForSnapName(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + setupChanges(st) + st.Unlock() + + // Execute + req, err := http.NewRequest("GET", "/v2/changes?for=funky-snap-name&select=all", nil) + c.Assert(err, check.IsNil) + rsp := getChanges(stateChangesCmd, req, nil).(*resp) + + // Verify + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + c.Assert(rsp.Result, check.FitsTypeOf, []*changeInfo(nil)) + + res := rsp.Result.([]*changeInfo) + c.Assert(res, check.HasLen, 1) + c.Check(res[0].Kind, check.Equals, `install`) + + _, err = rsp.MarshalJSON() + c.Assert(err, check.IsNil) +} + +func (s *apiSuite) TestStateChange(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + ids := setupChanges(st) + chg := st.Change(ids[0]) + chg.Set("api-data", map[string]int{"n": 42}) + st.Unlock() + s.vars = map[string]string{"id": ids[0]} + + // Execute + req, err := http.NewRequest("POST", "/v2/change/"+ids[0], nil) + c.Assert(err, check.IsNil) + rsp := getChange(stateChangeCmd, req, nil).(*resp) + rec := httptest.NewRecorder() + rsp.ServeHTTP(rec, req) + + // Verify + c.Check(rec.Code, check.Equals, 200) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.NotNil) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body["result"], check.DeepEquals, map[string]interface{}{ + "id": ids[0], + "kind": "install", + "summary": "install...", + "status": "Do", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "tasks": []interface{}{ + map[string]interface{}{ + "id": ids[2], + "kind": "download", + "summary": "1...", + "status": "Do", + "log": []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"}, + "progress": map[string]interface{}{"label": "", "done": 0., "total": 1.}, + "spawn-time": "2016-04-21T01:02:03Z", + }, + map[string]interface{}{ + "id": ids[3], + "kind": "activate", + "summary": "2...", + "status": "Do", + "progress": map[string]interface{}{"label": "", "done": 0., "total": 1.}, + "spawn-time": "2016-04-21T01:02:03Z", + }, + }, + "data": map[string]interface{}{ + "n": float64(42), + }, + }) +} + +func (s *apiSuite) TestStateChangeAbort(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + } + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + ids := setupChanges(st) + st.Unlock() + s.vars = map[string]string{"id": ids[0]} + + buf := bytes.NewBufferString(`{"action": "abort"}`) + + // Execute + req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf) + c.Assert(err, check.IsNil) + rsp := abortChange(stateChangeCmd, req, nil).(*resp) + rec := httptest.NewRecorder() + rsp.ServeHTTP(rec, req) + + // Ensure scheduled + c.Check(soon, check.Equals, 1) + + // Verify + c.Check(rec.Code, check.Equals, 200) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.NotNil) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body["result"], check.DeepEquals, map[string]interface{}{ + "id": ids[0], + "kind": "install", + "summary": "install...", + "status": "Hold", + "ready": true, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:03Z", + "tasks": []interface{}{ + map[string]interface{}{ + "id": ids[2], + "kind": "download", + "summary": "1...", + "status": "Hold", + "log": []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"}, + "progress": map[string]interface{}{"label": "", "done": 1., "total": 1.}, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:03Z", + }, + map[string]interface{}{ + "id": ids[3], + "kind": "activate", + "summary": "2...", + "status": "Hold", + "progress": map[string]interface{}{"label": "", "done": 1., "total": 1.}, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:03Z", + }, + }, + }) +} + +func (s *apiSuite) TestStateChangeAbortIsReady(c *check.C) { + restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) + defer restore() + + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + ids := setupChanges(st) + st.Change(ids[0]).SetStatus(state.DoneStatus) + st.Unlock() + s.vars = map[string]string{"id": ids[0]} + + buf := bytes.NewBufferString(`{"action": "abort"}`) + + // Execute + req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf) + c.Assert(err, check.IsNil) + rsp := abortChange(stateChangeCmd, req, nil).(*resp) + rec := httptest.NewRecorder() + rsp.ServeHTTP(rec, req) + + // Verify + c.Check(rec.Code, check.Equals, 400) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result, check.NotNil) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body["result"], check.DeepEquals, map[string]interface{}{ + "message": fmt.Sprintf("cannot abort change %s with nothing pending", ids[0]), + }) +} + +const validBuyInput = `{ + "snap-id": "the-snap-id-1234abcd", + "snap-name": "the snap name", + "price": 1.23, + "currency": "EUR" + }` + +var validBuyOptions = &store.BuyOptions{ + SnapID: "the-snap-id-1234abcd", + Price: 1.23, + Currency: "EUR", +} + +var buyTests = []struct { + input string + result *store.BuyResult + err error + expectedStatus int + expectedResult interface{} + expectedResponseType ResponseType + expectedBuyOptions *store.BuyOptions +}{ + { + // Success + input: validBuyInput, + result: &store.BuyResult{ + State: "Complete", + }, + expectedStatus: 200, + expectedResult: &store.BuyResult{ + State: "Complete", + }, + expectedResponseType: ResponseTypeSync, + expectedBuyOptions: validBuyOptions, + }, + { + // Fail with internal error + input: `{ + "snap-id": "the-snap-id-1234abcd", + "price": 1.23, + "currency": "EUR" + }`, + err: fmt.Errorf("internal error banana"), + expectedStatus: 500, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "internal error banana", + }, + expectedBuyOptions: &store.BuyOptions{ + SnapID: "the-snap-id-1234abcd", + Price: 1.23, + Currency: "EUR", + }, + }, + { + // Fail with unauthenticated error + input: validBuyInput, + err: store.ErrUnauthenticated, + expectedStatus: 400, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "you need to log in first", + Kind: "login-required", + }, + expectedBuyOptions: validBuyOptions, + }, + { + // Fail with TOS not accepted + input: validBuyInput, + err: store.ErrTOSNotAccepted, + expectedStatus: 400, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "terms of service not accepted", + Kind: "terms-not-accepted", + }, + expectedBuyOptions: validBuyOptions, + }, + { + // Fail with no payment methods + input: validBuyInput, + err: store.ErrNoPaymentMethods, + expectedStatus: 400, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "no payment methods", + Kind: "no-payment-methods", + }, + expectedBuyOptions: validBuyOptions, + }, + { + // Fail with payment declined + input: validBuyInput, + err: store.ErrPaymentDeclined, + expectedStatus: 400, + expectedResponseType: ResponseTypeError, + expectedResult: &errorResult{ + Message: "payment declined", + Kind: "payment-declined", + }, + expectedBuyOptions: validBuyOptions, + }, +} + +func (s *apiSuite) TestBuySnap(c *check.C) { + for _, test := range buyTests { + s.buyResult = test.result + s.err = test.err + + buf := bytes.NewBufferString(test.input) + req, err := http.NewRequest("POST", "/v2/buy", buf) + c.Assert(err, check.IsNil) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + rsp := postBuy(buyCmd, req, user).(*resp) + + c.Check(rsp.Status, check.Equals, test.expectedStatus) + c.Check(rsp.Type, check.Equals, test.expectedResponseType) + c.Assert(rsp.Result, check.FitsTypeOf, test.expectedResult) + c.Check(rsp.Result, check.DeepEquals, test.expectedResult) + + c.Check(s.buyOptions, check.DeepEquals, test.expectedBuyOptions) + c.Check(s.user, check.Equals, user) + } +} + +func (s *apiSuite) TestIsTrue(c *check.C) { + form := &multipart.Form{} + c.Check(isTrue(form, "foo"), check.Equals, false) + for _, f := range []string{"", "false", "0", "False", "f", "try"} { + form.Value = map[string][]string{"foo": {f}} + c.Check(isTrue(form, "foo"), check.Equals, false, check.Commentf("expected %q to be false", f)) + } + for _, t := range []string{"true", "1", "True", "t"} { + form.Value = map[string][]string{"foo": {t}} + c.Check(isTrue(form, "foo"), check.Equals, true, check.Commentf("expected %q to be true", t)) + } +} + +var readyToBuyTests = []struct { + input error + status int + respType interface{} + response interface{} +}{ + { + // Success + input: nil, + status: 200, + respType: ResponseTypeSync, + response: true, + }, + { + // Not accepted TOS + input: store.ErrTOSNotAccepted, + status: 400, + respType: ResponseTypeError, + response: &errorResult{ + Message: "terms of service not accepted", + Kind: errorKindTermsNotAccepted, + }, + }, + { + // No payment methods + input: store.ErrNoPaymentMethods, + status: 400, + respType: ResponseTypeError, + response: &errorResult{ + Message: "no payment methods", + Kind: errorKindNoPaymentMethods, + }, + }, +} + +func (s *apiSuite) TestReadyToBuy(c *check.C) { + for _, test := range readyToBuyTests { + s.err = test.input + + req, err := http.NewRequest("GET", "/v2/buy/ready", nil) + c.Assert(err, check.IsNil) + + state := snapCmd.d.overlord.State() + state.Lock() + user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) + state.Unlock() + c.Check(err, check.IsNil) + + rsp := readyToBuy(readyToBuyCmd, req, user).(*resp) + c.Check(rsp.Status, check.Equals, test.status) + c.Check(rsp.Type, check.Equals, test.respType) + c.Assert(rsp.Result, check.FitsTypeOf, test.response) + c.Check(rsp.Result, check.DeepEquals, test.response) + } +} + +var _ = check.Suite(&postCreateUserSuite{}) + +type postCreateUserSuite struct { + apiBaseSuite + + mockUserHome string +} + +func (s *postCreateUserSuite) SetUpTest(c *check.C) { + s.apiBaseSuite.SetUpTest(c) + + s.daemon(c) + postCreateUserUcrednetGet = func(string) (uint32, uint32, error) { + return 100, 0, nil + } + s.mockUserHome = c.MkDir() + userLookup = mkUserLookup(s.mockUserHome) +} + +func (s *postCreateUserSuite) TearDownTest(c *check.C) { + s.apiBaseSuite.TearDownTest(c) + + postCreateUserUcrednetGet = ucrednetGet + userLookup = user.Lookup + osutilAddUser = osutil.AddUser + storeUserInfo = store.UserInfo +} + +func mkUserLookup(userHomeDir string) func(string) (*user.User, error) { + return func(username string) (*user.User, error) { + cur, err := user.Current() + cur.Username = username + cur.HomeDir = userHomeDir + return cur, err + } +} + +func (s *postCreateUserSuite) TestPostCreateUserNoSSHKeys(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + storeUserInfo = func(user string) (*store.User, error) { + c.Check(user, check.Equals, "popper@lse.ac.uk") + return &store.User{ + Username: "karl", + OpenIDIdentifier: "xxyyzz", + }, nil + } + + buf := bytes.NewBufferString(`{"email": "popper@lse.ac.uk"}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user for "popper@lse.ac.uk": no ssh keys found`) +} + +func (s *postCreateUserSuite) TestPostCreateUser(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + storeUserInfo = func(user string) (*store.User, error) { + c.Check(user, check.Equals, "popper@lse.ac.uk") + return &store.User{ + Username: "karl", + SSHKeys: []string{"ssh1", "ssh2"}, + OpenIDIdentifier: "xxyyzz", + }, nil + } + osutilAddUser = func(username string, opts *osutil.AddUserOptions) error { + c.Check(username, check.Equals, "karl") + c.Check(opts.SSHKeys, check.DeepEquals, []string{"ssh1", "ssh2"}) + c.Check(opts.Gecos, check.Equals, "popper@lse.ac.uk,xxyyzz") + c.Check(opts.Sudoer, check.Equals, false) + return nil + } + + buf := bytes.NewBufferString(`{"email": "popper@lse.ac.uk"}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + expected := &userResponseData{ + Username: "karl", + SSHKeys: []string{"ssh1", "ssh2"}, + } + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + // user was setup in state + state := s.d.overlord.State() + state.Lock() + user, err := auth.User(state, 1) + state.Unlock() + c.Check(err, check.IsNil) + c.Check(user.Username, check.Equals, "karl") + c.Check(user.Email, check.Equals, "popper@lse.ac.uk") + c.Check(user.Macaroon, check.NotNil) + // auth saved to user home dir + outfile := filepath.Join(s.mockUserHome, ".snap", "auth.json") + c.Check(osutil.FileExists(outfile), check.Equals, true) + content, err := ioutil.ReadFile(outfile) + c.Check(err, check.IsNil) + c.Check(string(content), check.Equals, fmt.Sprintf(`{"macaroon":"%s"}`, user.Macaroon)) +} + +func (s *postCreateUserSuite) TestGetUserDetailsFromAssertionModelNotFound(c *check.C) { + st := s.d.overlord.State() + email := "foo@example.com" + + username, opts, err := getUserDetailsFromAssertion(st, email) + c.Check(username, check.Equals, "") + c.Check(opts, check.IsNil) + c.Check(err, check.ErrorMatches, `cannot add system-user "foo@example.com": cannot get model assertion: no state entry for key`) +} + +func (s *postCreateUserSuite) setupSigner(accountID string, signerPrivKey asserts.PrivateKey) *assertstest.SigningDB { + st := s.d.overlord.State() + + // create fake brand signature + signerSigning := assertstest.NewSigningDB(accountID, signerPrivKey) + + signerAcct := assertstest.NewAccount(s.storeSigning, accountID, map[string]interface{}{ + "account-id": accountID, + "verification": "certified", + }, "") + s.storeSigning.Add(signerAcct) + assertAdd(st, signerAcct) + + signerAccKey := assertstest.NewAccountKey(s.storeSigning, signerAcct, nil, signerPrivKey.PublicKey(), "") + s.storeSigning.Add(signerAccKey) + assertAdd(st, signerAccKey) + + return signerSigning +} + +var ( + brandPrivKey, _ = assertstest.GenerateKey(752) + partnerPrivKey, _ = assertstest.GenerateKey(752) + unknownPrivKey, _ = assertstest.GenerateKey(752) +) + +func (s *postCreateUserSuite) makeSystemUsers(c *check.C, systemUsers []map[string]interface{}) { + st := s.d.overlord.State() + + assertAdd(st, s.storeSigning.StoreAccountKey("")) + + brandSigning := s.setupSigner("my-brand", brandPrivKey) + partnerSigning := s.setupSigner("partner", partnerPrivKey) + unknownSigning := s.setupSigner("unknown", unknownPrivKey) + + signers := map[string]*assertstest.SigningDB{ + "my-brand": brandSigning, + "partner": partnerSigning, + "unknown": unknownSigning, + } + + model, err := brandSigning.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "authority-id": "my-brand", + "brand-id": "my-brand", + "model": "my-model", + "architecture": "amd64", + "gadget": "pc", + "kernel": "pc-kernel", + "required-snaps": []interface{}{"required-snap1"}, + "system-user-authority": []interface{}{"my-brand", "partner"}, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + model = model.(*asserts.Model) + + // now add model related stuff to the system + assertAdd(st, model) + + for _, suMap := range systemUsers { + su, err := signers[suMap["authority-id"].(string)].Sign(asserts.SystemUserType, suMap, nil, "") + c.Assert(err, check.IsNil) + su = su.(*asserts.SystemUser) + // now add system-user assertion to the system + assertAdd(st, su) + } + // create fake device + st.Lock() + err = auth.SetDevice(st, &auth.DeviceState{ + Brand: "my-brand", + Model: "my-model", + Serial: "serialserial", + }) + st.Unlock() + c.Assert(err, check.IsNil) +} + +var goodUser = map[string]interface{}{ + "authority-id": "my-brand", + "brand-id": "my-brand", + "email": "foo@bar.com", + "series": []interface{}{"16", "18"}, + "models": []interface{}{"my-model", "other-model"}, + "name": "Boring Guy", + "username": "guy", + "password": "$6$salt$hash", + "since": time.Now().Format(time.RFC3339), + "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339), +} + +var partnerUser = map[string]interface{}{ + "authority-id": "partner", + "brand-id": "my-brand", + "email": "p@partner.com", + "series": []interface{}{"16", "18"}, + "models": []interface{}{"my-model"}, + "name": "Partner Guy", + "username": "partnerguy", + "password": "$6$salt$hash", + "since": time.Now().Format(time.RFC3339), + "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339), +} + +var badUser = map[string]interface{}{ + // bad user (not valid for this model) + "authority-id": "my-brand", + "brand-id": "my-brand", + "email": "foobar@bar.com", + "series": []interface{}{"16", "18"}, + "models": []interface{}{"non-of-the-models-i-have"}, + "name": "Random Gal", + "username": "gal", + "password": "$6$salt$hash", + "since": time.Now().Format(time.RFC3339), + "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339), +} + +var unknownUser = map[string]interface{}{ + "authority-id": "unknown", + "brand-id": "my-brand", + "email": "x@partner.com", + "series": []interface{}{"16", "18"}, + "models": []interface{}{"my-model"}, + "name": "XGuy", + "username": "xguy", + "password": "$6$salt$hash", + "since": time.Now().Format(time.RFC3339), + "until": time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339), +} + +func (s *postCreateUserSuite) TestGetUserDetailsFromAssertionHappy(c *check.C) { + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + // ensure that if we query the details from the assert DB we get + // the expected user + st := s.d.overlord.State() + username, opts, err := getUserDetailsFromAssertion(st, "foo@bar.com") + c.Check(username, check.Equals, "guy") + c.Check(opts, check.DeepEquals, &osutil.AddUserOptions{ + Gecos: "foo@bar.com,Boring Guy", + Password: "$6$salt$hash", + }) + c.Check(err, check.IsNil) +} + +// FIXME: These tests all look similar, with small deltas. Would be +// nice to transform them into a table that is just the deltas, and +// run on a loop. +func (s *postCreateUserSuite) TestPostCreateUserFromAssertion(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + // mock the calls that create the user + osutilAddUser = func(username string, opts *osutil.AddUserOptions) error { + c.Check(username, check.Equals, "guy") + c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy") + c.Check(opts.Sudoer, check.Equals, false) + c.Check(opts.Password, check.Equals, "$6$salt$hash") + return nil + } + + defer func() { + osutilAddUser = osutil.AddUser + }() + + // do it! + buf := bytes.NewBufferString(`{"email": "foo@bar.com","known":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + expected := &userResponseData{ + Username: "guy", + } + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) + + // ensure the user was added to the state + st := s.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + c.Assert(err, check.IsNil) + st.Unlock() + c.Check(users, check.HasLen, 1) +} + +func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser, partnerUser, badUser, unknownUser}) + + // mock the calls that create the user + osutilAddUser = func(username string, opts *osutil.AddUserOptions) error { + switch username { + case "guy": + c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy") + case "partnerguy": + c.Check(opts.Gecos, check.Equals, "p@partner.com,Partner Guy") + default: + c.Logf("unexpected username %q", username) + c.Fail() + } + c.Check(opts.Sudoer, check.Equals, false) + c.Check(opts.Password, check.Equals, "$6$salt$hash") + return nil + } + defer func() { + osutilAddUser = osutil.AddUser + }() + + // do it! + buf := bytes.NewBufferString(`{"known":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + // note that we get a list here instead of a single + // userResponseData item + c.Check(rsp.Result, check.FitsTypeOf, []userResponseData{}) + seen := map[string]bool{} + for _, u := range rsp.Result.([]userResponseData) { + seen[u.Username] = true + c.Check(u, check.DeepEquals, userResponseData{Username: u.Username}) + } + c.Check(seen, check.DeepEquals, map[string]bool{ + "guy": true, + "partnerguy": true, + }) + + // ensure the user was added to the state + st := s.d.overlord.State() + st.Lock() + users, err := auth.Users(st) + c.Assert(err, check.IsNil) + st.Unlock() + c.Check(users, check.HasLen, 2) +} + +func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownClassicErrors(c *check.C) { + restore := release.MockOnClassic(true) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + postCreateUserUcrednetGet = func(string) (uint32, uint32, error) { + return 100, 0, nil + } + defer func() { + postCreateUserUcrednetGet = ucrednetGet + }() + + // do it! + buf := bytes.NewBufferString(`{"known":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user: device is a classic system`) +} + +func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownButOwnedErrors(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + st := s.d.overlord.State() + st.Lock() + _, err := auth.NewUser(st, "username", "email@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Check(err, check.IsNil) + + // do it! + buf := bytes.NewBufferString(`{"known":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user: device already managed`) +} + +func (s *postCreateUserSuite) TestPostCreateUserFromAssertionAllKnownButOwned(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + st := s.d.overlord.State() + st.Lock() + _, err := auth.NewUser(st, "username", "email@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Check(err, check.IsNil) + + // mock the calls that create the user + osutilAddUser = func(username string, opts *osutil.AddUserOptions) error { + c.Check(username, check.Equals, "guy") + c.Check(opts.Gecos, check.Equals, "foo@bar.com,Boring Guy") + c.Check(opts.Sudoer, check.Equals, false) + c.Check(opts.Password, check.Equals, "$6$salt$hash") + return nil + } + defer func() { + osutilAddUser = osutil.AddUser + }() + + // do it! + buf := bytes.NewBufferString(`{"known":true,"force-managed":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + // note that we get a list here instead of a single + // userResponseData item + expected := []userResponseData{ + {Username: "guy"}, + } + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *postCreateUserSuite) TestUsersEmpty(c *check.C) { + req, err := http.NewRequest("GET", "/v2/users", nil) + c.Assert(err, check.IsNil) + + rsp := getUsers(usersCmd, req, nil).(*resp) + + expected := []userResponseData{} + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *postCreateUserSuite) TestUsersHasUser(c *check.C) { + st := s.d.overlord.State() + st.Lock() + u, err := auth.NewUser(st, "someuser", "mymail@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Assert(err, check.IsNil) + + req, err := http.NewRequest("GET", "/v2/users", nil) + c.Assert(err, check.IsNil) + + rsp := getUsers(usersCmd, req, nil).(*resp) + + expected := []userResponseData{ + {ID: u.ID, Username: u.Username, Email: u.Email}, + } + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) +} + +func (s *postCreateUserSuite) TestSysInfoIsManaged(c *check.C) { + st := s.d.overlord.State() + st.Lock() + _, err := auth.NewUser(st, "someuser", "mymail@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Assert(err, check.IsNil) + + req, err := http.NewRequest("GET", "/v2/system-info", nil) + c.Assert(err, check.IsNil) + + rsp := sysInfo(sysInfoCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result.(map[string]interface{})["managed"], check.Equals, true) +} + +// aliases + +func (s *apiSuite) TestAliasSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &aliasAction{ + Action: "alias", + Snap: "alias-snap", + App: "app", + Alias: "alias1", + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Assert(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + // sanity check + c.Check(osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, true) +} + +func (s *apiSuite) TestAliasErrors(c *check.C) { + s.daemon(c) + + errScenarios := []struct { + mangle func(*aliasAction) + err string + }{ + {func(a *aliasAction) { a.Action = "" }, `unsupported alias action: ""`}, + {func(a *aliasAction) { a.Action = "what" }, `unsupported alias action: "what"`}, + {func(a *aliasAction) { a.Snap = "lalala" }, `snap "lalala" is not installed`}, + {func(a *aliasAction) { a.Alias = ".foo" }, `invalid alias name: ".foo"`}, + {func(a *aliasAction) { a.Aliases = []string{"baz"} }, `cannot interpret request, snaps can no longer be expected to declare their aliases`}, + } + + for _, scen := range errScenarios { + action := &aliasAction{ + Action: "alias", + Snap: "alias-snap", + App: "app", + Alias: "alias1", + } + scen.mangle(action) + + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + + rsp := changeAliases(aliasesCmd, req, nil).(*resp) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, scen.err) + } +} + +func (s *apiSuite) TestUnaliasSnapSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &aliasAction{ + Action: "unalias", + Snap: "alias-snap", + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Assert(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + c.Check(chg.Summary(), check.Equals, `Disable all aliases for snap "alias-snap"`) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + defer st.Unlock() + err = chg.Err() + c.Assert(err, check.IsNil) + + // sanity check + var snapst snapstate.SnapState + err = snapstate.Get(st, "alias-snap", &snapst) + c.Assert(err, check.IsNil) + c.Check(snapst.AutoAliasesDisabled, check.Equals, true) +} + +func (s *apiSuite) TestUnaliasDWIMSnapSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &aliasAction{ + Action: "unalias", + Snap: "alias-snap", + Alias: "alias-snap", + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Assert(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + c.Check(chg.Summary(), check.Equals, `Disable all aliases for snap "alias-snap"`) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + defer st.Unlock() + err = chg.Err() + c.Assert(err, check.IsNil) + + // sanity check + var snapst snapstate.SnapState + err = snapstate.Get(st, "alias-snap", &snapst) + c.Assert(err, check.IsNil) + c.Check(snapst.AutoAliasesDisabled, check.Equals, true) +} + +func (s *apiSuite) TestUnaliasAliasSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &aliasAction{ + Action: "alias", + Snap: "alias-snap", + App: "app", + Alias: "alias1", + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Assert(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + // unalias + action = &aliasAction{ + Action: "unalias", + Alias: "alias1", + } + text, err = json.Marshal(action) + c.Assert(err, check.IsNil) + buf = bytes.NewBuffer(text) + req, err = http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec = httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Assert(rec.Code, check.Equals, 202) + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id = body["change"].(string) + + st.Lock() + chg = st.Change(id) + c.Check(chg.Summary(), check.Equals, `Remove manual alias "alias1" for snap "alias-snap"`) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + defer st.Unlock() + err = chg.Err() + c.Assert(err, check.IsNil) + + // sanity check + c.Check(osutil.FileExists(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, false) +} + +func (s *apiSuite) TestUnaliasDWIMAliasSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &aliasAction{ + Action: "alias", + Snap: "alias-snap", + App: "app", + Alias: "alias1", + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Assert(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + err = chg.Err() + st.Unlock() + c.Assert(err, check.IsNil) + + // DWIM unalias an alias + action = &aliasAction{ + Action: "unalias", + Snap: "alias1", + Alias: "alias1", + } + text, err = json.Marshal(action) + c.Assert(err, check.IsNil) + buf = bytes.NewBuffer(text) + req, err = http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec = httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Assert(rec.Code, check.Equals, 202) + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id = body["change"].(string) + + st.Lock() + chg = st.Change(id) + c.Check(chg.Summary(), check.Equals, `Remove manual alias "alias1" for snap "alias-snap"`) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + defer st.Unlock() + err = chg.Err() + c.Assert(err, check.IsNil) + + // sanity check + c.Check(osutil.FileExists(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, false) +} + +func (s *apiSuite) TestPreferSuccess(c *check.C) { + err := os.MkdirAll(dirs.SnapBinariesDir, 0755) + c.Assert(err, check.IsNil) + d := s.daemon(c) + + s.mockSnap(c, aliasYaml) + + oldAutoAliases := snapstate.AutoAliases + snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { + return nil, nil + } + defer func() { snapstate.AutoAliases = oldAutoAliases }() + + d.overlord.Loop() + defer d.overlord.Stop() + + action := &aliasAction{ + Action: "prefer", + Snap: "alias-snap", + } + text, err := json.Marshal(action) + c.Assert(err, check.IsNil) + buf := bytes.NewBuffer(text) + req, err := http.NewRequest("POST", "/v2/aliases", buf) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) + c.Assert(rec.Code, check.Equals, 202) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + id := body["change"].(string) + + st := d.overlord.State() + st.Lock() + chg := st.Change(id) + c.Check(chg.Summary(), check.Equals, `Prefer aliases of snap "alias-snap"`) + st.Unlock() + c.Assert(chg, check.NotNil) + + <-chg.Ready() + + st.Lock() + defer st.Unlock() + err = chg.Err() + c.Assert(err, check.IsNil) + + // sanity check + var snapst snapstate.SnapState + err = snapstate.Get(st, "alias-snap", &snapst) + c.Assert(err, check.IsNil) + c.Check(snapst.AutoAliasesDisabled, check.Equals, false) +} + +func (s *apiSuite) TestAliases(c *check.C) { + d := s.daemon(c) + + st := d.overlord.State() + st.Lock() + snapstate.Set(st, "alias-snap1", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap1", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias1": {Manual: "cmd1x", Auto: "cmd1"}, + "alias2": {Auto: "cmd2"}, + }, + }) + snapstate.Set(st, "alias-snap2", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "alias-snap2", Revision: snap.R(12)}, + }, + Current: snap.R(12), + Active: true, + AutoAliasesDisabled: true, + Aliases: map[string]*snapstate.AliasTarget{ + "alias2": {Auto: "cmd2"}, + "alias3": {Manual: "cmd3"}, + "alias4": {Manual: "cmd4x", Auto: "cmd4"}, + }, + }) + st.Unlock() + + req, err := http.NewRequest("GET", "/v2/aliases", nil) + c.Assert(err, check.IsNil) + + rsp := getAliases(aliasesCmd, req, nil).(*resp) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Result, check.DeepEquals, map[string]map[string]aliasStatus{ + "alias-snap1": { + "alias1": { + Command: "alias-snap1.cmd1x", + Status: "manual", + Manual: "cmd1x", + Auto: "cmd1", + }, + "alias2": { + Command: "alias-snap1.cmd2", + Status: "auto", + Auto: "cmd2", + }, + }, + "alias-snap2": { + "alias2": { + Command: "alias-snap2.cmd2", + Status: "disabled", + Auto: "cmd2", + }, + "alias3": { + Command: "alias-snap2.cmd3", + Status: "manual", + Manual: "cmd3", + }, + "alias4": { + Command: "alias-snap2.cmd4x", + Status: "manual", + Manual: "cmd4x", + Auto: "cmd4", + }, + }, + }) + +} + +func (s *apiSuite) TestInstallUnaliased(c *check.C) { + var calledFlags snapstate.Flags + + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + // Install the snap without enabled automatic aliases + Unaliased: true, + Snaps: []string{"fake"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(calledFlags.Unaliased, check.Equals, true) +} + +func (s *apiSuite) TestSplitQS(c *check.C) { + c.Check(splitQS("foo,bar"), check.DeepEquals, []string{"foo", "bar"}) + c.Check(splitQS("foo , bar"), check.DeepEquals, []string{"foo", "bar"}) + c.Check(splitQS("foo ,, bar"), check.DeepEquals, []string{"foo", "bar"}) + c.Check(splitQS(""), check.HasLen, 0) + c.Check(splitQS(","), check.HasLen, 0) +} + +var _ = check.Suite(&postDebugSuite{}) + +type postDebugSuite struct { + apiBaseSuite +} + +func (s *postDebugSuite) TestPostDebugEnsureStateSoon(c *check.C) { + s.daemonWithOverlordMock(c) + + soon := 0 + ensureStateSoon = func(st *state.State) { + soon++ + ensureStateSoonImpl(st) + } + + buf := bytes.NewBufferString(`{"action": "ensure-state-soon"}`) + req, err := http.NewRequest("POST", "/v2/debug", buf) + c.Assert(err, check.IsNil) + + rsp := postDebug(debugCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.Equals, true) + c.Check(soon, check.Equals, 1) +} + +func (s *postDebugSuite) TestPostDebugGetBaseDeclaration(c *check.C) { + _ = s.daemon(c) + + buf := bytes.NewBufferString(`{"action": "get-base-declaration"}`) + req, err := http.NewRequest("POST", "/v2/debug", buf) + c.Assert(err, check.IsNil) + + rsp := postDebug(debugCmd, req, nil).(*resp) + + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result.(map[string]interface{})["base-declaration"], + testutil.Contains, "type: base-declaration") +} + +type appSuite struct { + apiBaseSuite + cmd *testutil.MockCmd + + infoA, infoB, infoC, infoD *snap.Info +} + +var _ = check.Suite(&appSuite{}) + +func (s *appSuite) SetUpTest(c *check.C) { + s.apiBaseSuite.SetUpTest(c) + s.cmd = testutil.MockCommand(c, "systemctl", "").Also("journalctl", "") + s.daemon(c) + s.infoA = s.mkInstalledInState(c, s.d, "snap-a", "dev", "v1", snap.R(1), true, "apps: {svc1: {daemon: simple}, svc2: {daemon: simple, reload-command: x}}") + s.infoB = s.mkInstalledInState(c, s.d, "snap-b", "dev", "v1", snap.R(1), true, "apps: {svc3: {daemon: simple}, cmd1: {}}") + s.infoC = s.mkInstalledInState(c, s.d, "snap-c", "dev", "v1", snap.R(1), true, "") + s.infoD = s.mkInstalledInState(c, s.d, "snap-d", "dev", "v1", snap.R(1), true, "apps: {cmd2: {}, cmd3: {}}") + s.d.overlord.Loop() +} + +func (s *appSuite) TearDownTest(c *check.C) { + s.d.overlord.Stop() + s.cmd.Restore() + s.apiBaseSuite.TearDownTest(c) +} + +func (s *appSuite) TestSplitAppName(c *check.C) { + type T struct { + name string + snap string + app string + } + + for _, x := range []T{ + {name: "foo.bar", snap: "foo", app: "bar"}, + {name: "foo", snap: "foo", app: ""}, + {name: "foo.bar.baz", snap: "foo", app: "bar.baz"}, + {name: ".", snap: "", app: ""}, // SISO + } { + snap, app := splitAppName(x.name) + c.Check(x.snap, check.Equals, snap, check.Commentf(x.name)) + c.Check(x.app, check.Equals, app, check.Commentf(x.name)) + } +} + +func (s *appSuite) TestGetAppsInfo(c *check.C) { + svcNames := []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"} + for _, name := range svcNames { + s.sysctlBufs = append(s.sysctlBufs, []byte(fmt.Sprintf(` +Id=snap.%s.service +Type=simple +ActiveState=active +UnitFileState=enabled +`[1:], name))) + } + + req, err := http.NewRequest("GET", "/v2/apps", nil) + c.Assert(err, check.IsNil) + + rsp := getAppsInfo(appsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 200) + c.Assert(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{}) + apps := rsp.Result.([]client.AppInfo) + c.Assert(apps, check.HasLen, 6) + + for _, name := range svcNames { + snap, app := splitAppName(name) + c.Check(apps, testutil.DeepContains, client.AppInfo{ + Snap: snap, + Name: app, + Daemon: "simple", + Active: true, + Enabled: true, + }) + } + + for _, name := range []string{"snap-b.cmd1", "snap-d.cmd2", "snap-d.cmd3"} { + snap, app := splitAppName(name) + c.Check(apps, testutil.DeepContains, client.AppInfo{ + Snap: snap, + Name: app, + }) + } + + appNames := make([]string, len(apps)) + for i, app := range apps { + appNames[i] = app.Snap + "." + app.Name + } + c.Check(sort.StringsAreSorted(appNames), check.Equals, true) +} + +func (s *appSuite) TestGetAppsInfoNames(c *check.C) { + + req, err := http.NewRequest("GET", "/v2/apps?names=snap-d", nil) + c.Assert(err, check.IsNil) + + rsp := getAppsInfo(appsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 200) + c.Assert(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{}) + apps := rsp.Result.([]client.AppInfo) + c.Assert(apps, check.HasLen, 2) + + for _, name := range []string{"snap-d.cmd2", "snap-d.cmd3"} { + snap, app := splitAppName(name) + c.Check(apps, testutil.DeepContains, client.AppInfo{ + Snap: snap, + Name: app, + }) + } + + appNames := make([]string, len(apps)) + for i, app := range apps { + appNames[i] = app.Snap + "." + app.Name + } + c.Check(sort.StringsAreSorted(appNames), check.Equals, true) +} + +func (s *appSuite) TestGetAppsInfoServices(c *check.C) { + svcNames := []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"} + for _, name := range svcNames { + s.sysctlBufs = append(s.sysctlBufs, []byte(fmt.Sprintf(` +Id=snap.%s.service +Type=simple +ActiveState=active +UnitFileState=enabled +`[1:], name))) + } + + req, err := http.NewRequest("GET", "/v2/apps?select=service", nil) + c.Assert(err, check.IsNil) + + rsp := getAppsInfo(appsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 200) + c.Assert(rsp.Type, check.Equals, ResponseTypeSync) + c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{}) + svcs := rsp.Result.([]client.AppInfo) + c.Assert(svcs, check.HasLen, 3) + + for _, name := range svcNames { + snap, app := splitAppName(name) + c.Check(svcs, testutil.DeepContains, client.AppInfo{ + Snap: snap, + Name: app, + Daemon: "simple", + Active: true, + Enabled: true, + }) + } + + appNames := make([]string, len(svcs)) + for i, svc := range svcs { + appNames[i] = svc.Snap + "." + svc.Name + } + c.Check(sort.StringsAreSorted(appNames), check.Equals, true) +} + +func (s *appSuite) TestGetAppsInfoBadSelect(c *check.C) { + req, err := http.NewRequest("GET", "/v2/apps?select=potato", nil) + c.Assert(err, check.IsNil) + + rsp := getAppsInfo(appsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 400) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) +} + +func (s *appSuite) TestGetAppsInfoBadName(c *check.C) { + req, err := http.NewRequest("GET", "/v2/apps?names=potato", nil) + c.Assert(err, check.IsNil) + + rsp := getAppsInfo(appsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 404) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) +} + +func (s *appSuite) TestAppInfosForOne(c *check.C) { + st := s.d.overlord.State() + appInfos, rsp := appInfosFor(st, []string{"snap-a.svc1"}, appInfoOptions{service: true}) + c.Assert(rsp, check.IsNil) + c.Assert(appInfos, check.HasLen, 1) + c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA) + c.Check(appInfos[0].Name, check.Equals, "svc1") +} + +func (s *appSuite) TestAppInfosForAll(c *check.C) { + type T struct { + opts appInfoOptions + snaps []*snap.Info + names []string + } + + for _, t := range []T{ + { + opts: appInfoOptions{service: true}, + names: []string{"svc1", "svc2", "svc3"}, + snaps: []*snap.Info{s.infoA, s.infoA, s.infoB}, + }, + { + opts: appInfoOptions{}, + names: []string{"svc1", "svc2", "cmd1", "svc3", "cmd2", "cmd3"}, + snaps: []*snap.Info{s.infoA, s.infoA, s.infoB, s.infoB, s.infoD, s.infoD}, + }, + } { + c.Assert(len(t.names), check.Equals, len(t.snaps), check.Commentf("%s", t.opts)) + + st := s.d.overlord.State() + appInfos, rsp := appInfosFor(st, nil, t.opts) + c.Assert(rsp, check.IsNil, check.Commentf("%s", t.opts)) + names := make([]string, len(appInfos)) + for i, appInfo := range appInfos { + names[i] = appInfo.Name + } + c.Assert(names, check.DeepEquals, t.names, check.Commentf("%s", t.opts)) + + for i := range appInfos { + c.Check(appInfos[i].Snap, check.DeepEquals, t.snaps[i], check.Commentf("%s: %s", t.opts, t.names[i])) + } + } +} + +func (s *appSuite) TestAppInfosForOneSnap(c *check.C) { + st := s.d.overlord.State() + appInfos, rsp := appInfosFor(st, []string{"snap-a"}, appInfoOptions{service: true}) + c.Assert(rsp, check.IsNil) + c.Assert(appInfos, check.HasLen, 2) + sort.Sort(bySnapApp(appInfos)) + + c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA) + c.Check(appInfos[0].Name, check.Equals, "svc1") + c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA) + c.Check(appInfos[1].Name, check.Equals, "svc2") +} + +func (s *appSuite) TestAppInfosForMixedArgs(c *check.C) { + st := s.d.overlord.State() + appInfos, rsp := appInfosFor(st, []string{"snap-a", "snap-a.svc1"}, appInfoOptions{service: true}) + c.Assert(rsp, check.IsNil) + c.Assert(appInfos, check.HasLen, 2) + sort.Sort(bySnapApp(appInfos)) + + c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA) + c.Check(appInfos[0].Name, check.Equals, "svc1") + c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA) + c.Check(appInfos[1].Name, check.Equals, "svc2") +} + +func (s *appSuite) TestAppInfosCleanupAndSorted(c *check.C) { + st := s.d.overlord.State() + appInfos, rsp := appInfosFor(st, []string{ + "snap-b.svc3", + "snap-a.svc2", + "snap-a.svc1", + "snap-a.svc2", + "snap-b.svc3", + "snap-a.svc1", + "snap-b", + "snap-a", + }, appInfoOptions{service: true}) + c.Assert(rsp, check.IsNil) + c.Assert(appInfos, check.HasLen, 3) + sort.Sort(bySnapApp(appInfos)) + + c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA) + c.Check(appInfos[0].Name, check.Equals, "svc1") + c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA) + c.Check(appInfos[1].Name, check.Equals, "svc2") + c.Check(appInfos[2].Snap, check.DeepEquals, s.infoB) + c.Check(appInfos[2].Name, check.Equals, "svc3") +} + +func (s *appSuite) TestAppInfosForAppless(c *check.C) { + st := s.d.overlord.State() + appInfos, rsp := appInfosFor(st, []string{"snap-c"}, appInfoOptions{service: true}) + c.Assert(rsp, check.FitsTypeOf, &resp{}) + c.Check(rsp.(*resp).Status, check.Equals, 404) + c.Check(rsp.(*resp).Result.(*errorResult).Kind, check.Equals, errorKindAppNotFound) + c.Assert(appInfos, check.IsNil) +} + +func (s *appSuite) TestAppInfosForMissingApp(c *check.C) { + st := s.d.overlord.State() + appInfos, rsp := appInfosFor(st, []string{"snap-c.whatever"}, appInfoOptions{service: true}) + c.Assert(rsp, check.FitsTypeOf, &resp{}) + c.Check(rsp.(*resp).Status, check.Equals, 404) + c.Check(rsp.(*resp).Result.(*errorResult).Kind, check.Equals, errorKindAppNotFound) + c.Assert(appInfos, check.IsNil) +} + +func (s *appSuite) TestAppInfosForMissingSnap(c *check.C) { + st := s.d.overlord.State() + appInfos, rsp := appInfosFor(st, []string{"snap-x"}, appInfoOptions{service: true}) + c.Assert(rsp, check.FitsTypeOf, &resp{}) + c.Check(rsp.(*resp).Status, check.Equals, 404) + c.Check(rsp.(*resp).Result.(*errorResult).Kind, check.Equals, errorKindSnapNotFound) + c.Assert(appInfos, check.IsNil) +} + +func (s *apiSuite) TestLogsNoServices(c *check.C) { + // NOTE this is *apiSuite, not *appSuite, so there are no + // installed snaps with services + + cmd := testutil.MockCommand(c, "systemctl", "").Also("journalctl", "") + defer cmd.Restore() + s.daemon(c) + s.d.overlord.Loop() + defer s.d.overlord.Stop() + + req, err := http.NewRequest("GET", "/v2/logs", nil) + c.Assert(err, check.IsNil) + + rsp := getLogs(logsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 404) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) +} + +func (s *appSuite) TestLogs(c *check.C) { + s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(` +{"MESSAGE": "hello1", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "42"} +{"MESSAGE": "hello2", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "44"} +{"MESSAGE": "hello3", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "46"} +{"MESSAGE": "hello4", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "48"} +{"MESSAGE": "hello5", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "50"} + `))} + + req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + getLogs(logsCmd, req, nil).ServeHTTP(rec, req) + + c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}}) + c.Check(s.jctlNs, check.DeepEquals, []string{"42"}) + c.Check(s.jctlFollows, check.DeepEquals, []bool{false}) + + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json-seq") + c.Check(rec.Body.String(), check.Equals, ` +{"timestamp":"1970-01-01T00:00:00.000042Z","message":"hello1","sid":"xyzzy","pid":"42"} +{"timestamp":"1970-01-01T00:00:00.000044Z","message":"hello2","sid":"xyzzy","pid":"42"} +{"timestamp":"1970-01-01T00:00:00.000046Z","message":"hello3","sid":"xyzzy","pid":"42"} +{"timestamp":"1970-01-01T00:00:00.000048Z","message":"hello4","sid":"xyzzy","pid":"42"} +{"timestamp":"1970-01-01T00:00:00.00005Z","message":"hello5","sid":"xyzzy","pid":"42"} +`[1:]) +} + +func (s *appSuite) TestLogsN(c *check.C) { + type T struct { + in string + out string + } + + for _, t := range []T{ + {in: "", out: "10"}, + {in: "0", out: "0"}, + {in: "-1", out: "all"}, + {in: strconv.Itoa(math.MinInt32), out: "all"}, + {in: strconv.Itoa(math.MaxInt32), out: strconv.Itoa(math.MaxInt32)}, + } { + + s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(""))} + s.jctlNs = nil + + req, err := http.NewRequest("GET", "/v2/logs?n="+t.in, nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + getLogs(logsCmd, req, nil).ServeHTTP(rec, req) + + c.Check(s.jctlNs, check.DeepEquals, []string{t.out}) + } +} + +func (s *appSuite) TestLogsBadN(c *check.C) { + req, err := http.NewRequest("GET", "/v2/logs?n=hello", nil) + c.Assert(err, check.IsNil) + + rsp := getLogs(logsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 400) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) +} + +func (s *appSuite) TestLogsFollow(c *check.C) { + s.jctlRCs = []io.ReadCloser{ + ioutil.NopCloser(strings.NewReader("")), + ioutil.NopCloser(strings.NewReader("")), + ioutil.NopCloser(strings.NewReader("")), + } + + reqT, err := http.NewRequest("GET", "/v2/logs?follow=true", nil) + c.Assert(err, check.IsNil) + reqF, err := http.NewRequest("GET", "/v2/logs?follow=false", nil) + c.Assert(err, check.IsNil) + reqN, err := http.NewRequest("GET", "/v2/logs", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + getLogs(logsCmd, reqT, nil).ServeHTTP(rec, reqT) + getLogs(logsCmd, reqF, nil).ServeHTTP(rec, reqF) + getLogs(logsCmd, reqN, nil).ServeHTTP(rec, reqN) + + c.Check(s.jctlFollows, check.DeepEquals, []bool{true, false, false}) +} + +func (s *appSuite) TestLogsBadFollow(c *check.C) { + req, err := http.NewRequest("GET", "/v2/logs?follow=hello", nil) + c.Assert(err, check.IsNil) + + rsp := getLogs(logsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 400) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) +} + +func (s *appSuite) TestLogsBadName(c *check.C) { + req, err := http.NewRequest("GET", "/v2/logs?names=hello", nil) + c.Assert(err, check.IsNil) + + rsp := getLogs(logsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 404) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) +} + +func (s *appSuite) TestLogsSad(c *check.C) { + s.jctlErrs = []error{errors.New("potato")} + req, err := http.NewRequest("GET", "/v2/logs", nil) + c.Assert(err, check.IsNil) + + rsp := getLogs(logsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 500) + c.Assert(rsp.Type, check.Equals, ResponseTypeError) +} + +func (s *appSuite) testPostApps(c *check.C, inst servicestate.Instruction, systemctlCall []string) *state.Change { + postBody, err := json.Marshal(inst) + c.Assert(err, check.IsNil) + + req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBuffer(postBody)) + c.Assert(err, check.IsNil) + + rsp := postApps(appsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 202) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) + c.Check(rsp.Change, check.Matches, `[0-9]+`) + + st := s.d.overlord.State() + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Tasks(), check.HasLen, 1) + + st.Unlock() + <-chg.Ready() + st.Lock() + + c.Check(s.cmd.Calls(), check.DeepEquals, [][]string{systemctlCall}) + return chg +} + +func (s *appSuite) TestPostAppsStartOne(c *check.C) { + inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}} + expected := []string{"systemctl", "start", "snap.snap-a.svc2.service"} + s.testPostApps(c, inst, expected) +} + +func (s *appSuite) TestPostAppsStartTwo(c *check.C) { + inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a"}} + expected := []string{"systemctl", "start", "snap.snap-a.svc1.service", "snap.snap-a.svc2.service"} + chg := s.testPostApps(c, inst, expected) + chg.State().Lock() + defer chg.State().Unlock() + // check the summary expands the snap into actual apps + c.Check(chg.Summary(), check.Equals, "Running service command") + c.Check(chg.Tasks()[0].Summary(), check.Equals, "start of [snap-a.svc1 snap-a.svc2]") +} + +func (s *appSuite) TestPostAppsStartThree(c *check.C) { + inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a", "snap-b"}} + expected := []string{"systemctl", "start", "snap.snap-a.svc1.service", "snap.snap-a.svc2.service", "snap.snap-b.svc3.service"} + chg := s.testPostApps(c, inst, expected) + // check the summary expands the snap into actual apps + c.Check(chg.Summary(), check.Equals, "Running service command") + chg.State().Lock() + defer chg.State().Unlock() + c.Check(chg.Tasks()[0].Summary(), check.Equals, "start of [snap-a.svc1 snap-a.svc2 snap-b.svc3]") +} + +func (s *appSuite) TestPosetAppsStop(c *check.C) { + inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}} + expected := []string{"systemctl", "stop", "snap.snap-a.svc2.service"} + s.testPostApps(c, inst, expected) +} + +func (s *appSuite) TestPosetAppsRestart(c *check.C) { + inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}} + expected := []string{"systemctl", "restart", "snap.snap-a.svc2.service"} + s.testPostApps(c, inst, expected) +} + +func (s *appSuite) TestPosetAppsReload(c *check.C) { + inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}} + inst.Reload = true + expected := []string{"systemctl", "reload-or-restart", "snap.snap-a.svc2.service"} + s.testPostApps(c, inst, expected) +} + +func (s *appSuite) TestPosetAppsEnableNow(c *check.C) { + inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}} + inst.Enable = true + expected := []string{"systemctl", "enable", "--now", "snap.snap-a.svc2.service"} + s.testPostApps(c, inst, expected) +} + +func (s *appSuite) TestPosetAppsDisableNow(c *check.C) { + inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}} + inst.Disable = true + expected := []string{"systemctl", "disable", "--now", "snap.snap-a.svc2.service"} + s.testPostApps(c, inst, expected) +} + +func (s *appSuite) TestPostAppsBadJSON(c *check.C) { + req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`'junk`)) + c.Assert(err, check.IsNil) + rsp := postApps(appsCmd, req, nil).(*resp) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, ".*cannot decode request body.*") +} + +func (s *appSuite) TestPostAppsBadOp(c *check.C) { + req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"random": "json"}`)) + c.Assert(err, check.IsNil) + rsp := postApps(appsCmd, req, nil).(*resp) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, ".*cannot perform operation on services without a list of services.*") +} + +func (s *appSuite) TestPostAppsBadSnap(c *check.C) { + req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-c"]}`)) + c.Assert(err, check.IsNil) + rsp := postApps(appsCmd, req, nil).(*resp) + c.Check(rsp.Status, check.Equals, 404) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, `snap "snap-c" has no services`) +} + +func (s *appSuite) TestPostAppsBadApp(c *check.C) { + req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-a.what"]}`)) + c.Assert(err, check.IsNil) + rsp := postApps(appsCmd, req, nil).(*resp) + c.Check(rsp.Status, check.Equals, 404) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, `snap "snap-a" has no service "what"`) +} + +func (s *appSuite) TestPostAppsBadAction(c *check.C) { + req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "discombobulate", "names": ["snap-a.svc1"]}`)) + c.Assert(err, check.IsNil) + rsp := postApps(appsCmd, req, nil).(*resp) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, `unknown action "discombobulate"`) +} + +func (s *appSuite) TestPostAppsConflict(c *check.C) { + st := s.d.overlord.State() + st.Lock() + locked := true + defer func() { + if locked { + st.Unlock() + } + }() + + ts, err := snapstate.Remove(st, "snap-a", snap.R(0)) + c.Assert(err, check.IsNil) + // need a change to make the tasks visible + st.NewChange("enable", "...").AddAll(ts) + st.Unlock() + locked = false + + req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`)) + c.Assert(err, check.IsNil) + rsp := postApps(appsCmd, req, nil).(*resp) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Check(rsp.Result.(*errorResult).Message, check.Equals, `snap "snap-a" has changes in progress`) +} diff --git a/daemon/daemon.go b/daemon/daemon.go new file mode 100644 index 00000000..92f0012b --- /dev/null +++ b/daemon/daemon.go @@ -0,0 +1,484 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "fmt" + "net" + "net/http" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "sync" + unix "syscall" + "time" + + "github.com/coreos/go-systemd/activation" + "github.com/gorilla/mux" + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/polkit" +) + +// A Daemon listens for requests and routes them to the right command +type Daemon struct { + Version string + overlord *overlord.Overlord + snapdListener net.Listener + snapdServe *shutdownServer + snapListener net.Listener + snapServe *shutdownServer + tomb tomb.Tomb + router *mux.Router + // enableInternalInterfaceActions controls if adding and removing slots and plugs is allowed. + enableInternalInterfaceActions bool +} + +// A ResponseFunc handles one of the individual verbs for a method +type ResponseFunc func(*Command, *http.Request, *auth.UserState) Response + +// A Command routes a request to an individual per-verb ResponseFUnc +type Command struct { + Path string + // + GET ResponseFunc + PUT ResponseFunc + POST ResponseFunc + DELETE ResponseFunc + // can guest GET? + GuestOK bool + // can non-admin GET? + UserOK bool + // is this path accessible on the snapd-snap socket? + SnapOK bool + + // can polkit grant access? set to polkit action ID if so + PolkitOK string + + d *Daemon +} + +type accessResult int + +const ( + accessOK accessResult = iota + accessUnauthorized + accessForbidden +) + +var polkitCheckAuthorizationForPid = polkit.CheckAuthorizationForPid + +func (c *Command) canAccess(r *http.Request, user *auth.UserState) accessResult { + if user != nil { + // Authenticated users do anything for now. + return accessOK + } + + isUser := false + pid, uid, err := ucrednetGet(r.RemoteAddr) + if err == nil { + isUser = true + } else if err != errNoID { + logger.Noticef("unexpected error when attempting to get UID: %s", err) + return accessForbidden + } else if c.SnapOK { + return accessOK + } + + if r.Method == "GET" { + // Guest and user access restricted to GET requests + if c.GuestOK { + return accessOK + } + + if isUser && c.UserOK { + return accessOK + } + } + + // Remaining admin checks rely on identifying peer uid + if !isUser { + return accessUnauthorized + } + + if uid == 0 { + // Superuser does anything. + return accessOK + } + + if c.PolkitOK != "" { + var flags polkit.CheckFlags + allowHeader := r.Header.Get(client.AllowInteractionHeader) + if allowHeader != "" { + if allow, err := strconv.ParseBool(allowHeader); err != nil { + logger.Noticef("error parsing %s header: %s", client.AllowInteractionHeader, err) + } else if allow { + flags |= polkit.CheckAllowInteraction + } + } + if authorized, err := polkitCheckAuthorizationForPid(pid, c.PolkitOK, nil, flags); err == nil { + if authorized { + // polkit says user is authorised + return accessOK + } + } else if err == polkit.ErrDismissed { + return accessForbidden + } else { + logger.Noticef("polkit error: %s", err) + } + } + + return accessUnauthorized +} + +func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) { + state := c.d.overlord.State() + state.Lock() + // TODO Look at the error and fail if there's an attempt to authenticate with invalid data. + user, _ := UserFromRequest(state, r) + state.Unlock() + + switch c.canAccess(r, user) { + case accessOK: + // nothing + case accessUnauthorized: + Unauthorized("access denied").ServeHTTP(w, r) + return + case accessForbidden: + Forbidden("forbidden").ServeHTTP(w, r) + return + } + + var rspf ResponseFunc + var rsp = MethodNotAllowed("method %q not allowed", r.Method) + + switch r.Method { + case "GET": + rspf = c.GET + case "PUT": + rspf = c.PUT + case "POST": + rspf = c.POST + case "DELETE": + rspf = c.DELETE + } + + if rspf != nil { + rsp = rspf(c, r, user) + } + + rsp.ServeHTTP(w, r) +} + +type wrappedWriter struct { + w http.ResponseWriter + s int +} + +func (w *wrappedWriter) Header() http.Header { + return w.w.Header() +} + +func (w *wrappedWriter) Write(bs []byte) (int, error) { + return w.w.Write(bs) +} + +func (w *wrappedWriter) WriteHeader(s int) { + w.w.WriteHeader(s) + w.s = s +} + +func (w *wrappedWriter) Flush() { + if f, ok := w.w.(http.Flusher); ok { + f.Flush() + } +} + +func logit(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ww := &wrappedWriter{w: w} + t0 := time.Now() + handler.ServeHTTP(ww, r) + t := time.Now().Sub(t0) + url := r.URL.String() + if !strings.Contains(url, "/changes/") { + logger.Debugf("%s %s %s %s %d", r.RemoteAddr, r.Method, r.URL, t, ww.s) + } + }) +} + +// getListener tries to get a listener for the given socket path from +// the listener map, and if it fails it tries to set it up directly. +func getListener(socketPath string, listenerMap map[string]net.Listener) (net.Listener, error) { + if listener, ok := listenerMap[socketPath]; ok { + return listener, nil + } + + if c, err := net.Dial("unix", socketPath); err == nil { + c.Close() + return nil, fmt.Errorf("socket %q already in use", socketPath) + } + + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + return nil, err + } + + address, err := net.ResolveUnixAddr("unix", socketPath) + if err != nil { + return nil, err + } + + runtime.LockOSThread() + oldmask := unix.Umask(0111) + listener, err := net.ListenUnix("unix", address) + unix.Umask(oldmask) + runtime.UnlockOSThread() + if err != nil { + return nil, err + } + + logger.Debugf("socket %q was not activated; listening", socketPath) + + return listener, nil +} + +// Init sets up the Daemon's internal workings. +// Don't call more than once. +func (d *Daemon) Init() error { + listeners, err := activation.Listeners(false) + if err != nil { + return err + } + + listenerMap := make(map[string]net.Listener, len(listeners)) + + for _, listener := range listeners { + listenerMap[listener.Addr().String()] = listener + } + + // The SnapdSocket is required-- without it, die. + if listener, err := getListener(dirs.SnapdSocket, listenerMap); err == nil { + d.snapdListener = &ucrednetListener{listener} + } else { + return fmt.Errorf("when trying to listen on %s: %v", dirs.SnapdSocket, err) + } + + if listener, err := getListener(dirs.SnapSocket, listenerMap); err == nil { + // Note that the SnapSocket listener does not use ucrednet. We use the lack + // of remote information as an indication that the request originated with + // this socket. This listener may also be nil if that socket wasn't among + // the listeners, so check it before using it. + d.snapListener = listener + } else { + logger.Debugf("cannot get listener for %q: %v", dirs.SnapSocket, err) + } + + d.addRoutes() + + logger.Noticef("started %v.", httputil.UserAgent()) + + return nil +} + +func (d *Daemon) addRoutes() { + d.router = mux.NewRouter() + + for _, c := range api { + c.d = d + d.router.Handle(c.Path, c).Name(c.Path) + } + + // also maybe add a /favicon.ico handler... + + d.router.NotFoundHandler = NotFound("not found") +} + +var ( + shutdownTimeout = 5 * time.Second +) + +// shutdownServer supplements a http.Server with graceful shutdown. +// TODO: with go1.8 http.Server itself grows a graceful Shutdown method +type shutdownServer struct { + l net.Listener + httpSrv *http.Server + + mu sync.Mutex + conns map[net.Conn]http.ConnState + shuttingDown bool +} + +func newShutdownServer(l net.Listener, h http.Handler) *shutdownServer { + srv := &http.Server{ + Handler: h, + } + ssrv := &shutdownServer{ + l: l, + httpSrv: srv, + conns: make(map[net.Conn]http.ConnState), + } + srv.ConnState = ssrv.trackConn + return ssrv +} + +func (srv *shutdownServer) Serve() error { + return srv.httpSrv.Serve(srv.l) +} + +func (srv *shutdownServer) trackConn(conn net.Conn, state http.ConnState) { + srv.mu.Lock() + defer srv.mu.Unlock() + // we ignore hijacked connections, if we do things with websockets + // we'll need custom shutdown handling for them + if state == http.StateClosed || state == http.StateHijacked { + delete(srv.conns, conn) + return + } + if srv.shuttingDown && state == http.StateIdle { + conn.Close() + delete(srv.conns, conn) + return + } + srv.conns[conn] = state +} + +func (srv *shutdownServer) finishShutdown() error { + toutC := time.After(shutdownTimeout) + + srv.mu.Lock() + defer srv.mu.Unlock() + + srv.shuttingDown = true + for conn, state := range srv.conns { + if state == http.StateIdle { + conn.Close() + delete(srv.conns, conn) + } + } + + doWait := true + for doWait { + if len(srv.conns) == 0 { + return nil + } + srv.mu.Unlock() + select { + case <-time.After(200 * time.Millisecond): + case <-toutC: + doWait = false + } + srv.mu.Lock() + } + return fmt.Errorf("cannot gracefully finish, still active connections on %v after %v", srv.l.Addr(), shutdownTimeout) +} + +var shutdownMsg = i18n.G("reboot scheduled to update the system - temporarily cancel with 'sudo shutdown -c'") + +// Start the Daemon +func (d *Daemon) Start() { + // die when asked to restart (systemd should get us back up!) + d.overlord.SetRestartHandler(func(t state.RestartType) { + switch t { + case state.RestartDaemon: + d.tomb.Kill(nil) + case state.RestartSystem: + cmd := exec.Command("shutdown", "+10", "-r", shutdownMsg) + if out, err := cmd.CombinedOutput(); err != nil { + logger.Noticef("%s", osutil.OutputErr(out, err)) + } + default: + logger.Noticef("internal error: restart handler called with unknown restart type: %v", t) + d.tomb.Kill(nil) + } + }) + + if d.snapListener != nil { + d.snapServe = newShutdownServer(d.snapListener, logit(d.router)) + } + d.snapdServe = newShutdownServer(d.snapdListener, logit(d.router)) + + // the loop runs in its own goroutine + d.overlord.Loop() + + d.tomb.Go(func() error { + if d.snapListener != nil { + d.tomb.Go(func() error { + if err := d.snapServe.Serve(); err != nil && d.tomb.Err() == tomb.ErrStillAlive { + return err + } + + return nil + }) + } + + if err := d.snapdServe.Serve(); err != nil && d.tomb.Err() == tomb.ErrStillAlive { + return err + } + + return nil + }) +} + +// Stop shuts down the Daemon +func (d *Daemon) Stop() error { + d.tomb.Kill(nil) + d.snapdListener.Close() + if d.snapListener != nil { + d.snapListener.Close() + } + + d.tomb.Kill(d.snapdServe.finishShutdown()) + if d.snapListener != nil { + d.tomb.Kill(d.snapServe.finishShutdown()) + } + + d.overlord.Stop() + + return d.tomb.Wait() +} + +// Dying is a tomb-ish thing +func (d *Daemon) Dying() <-chan struct{} { + return d.tomb.Dying() +} + +// New Daemon +func New() (*Daemon, error) { + ovld, err := overlord.New() + if err != nil { + return nil, err + } + return &Daemon{ + overlord: ovld, + // TODO: Decide when this should be disabled by default. + enableInternalInterfaceActions: true, + }, nil +} diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go new file mode 100644 index 00000000..00558a0d --- /dev/null +++ b/daemon/daemon_test.go @@ -0,0 +1,527 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "fmt" + + "bytes" + "errors" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/gorilla/mux" + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/polkit" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { check.TestingT(t) } + +type daemonSuite struct { + authorized bool + err error + lastPolkitFlags polkit.CheckFlags +} + +var _ = check.Suite(&daemonSuite{}) + +func (s *daemonSuite) checkAuthorizationForPid(pid uint32, actionId string, details map[string]string, flags polkit.CheckFlags) (bool, error) { + s.lastPolkitFlags = flags + return s.authorized, s.err +} + +func (s *daemonSuite) SetUpTest(c *check.C) { + dirs.SetRootDir(c.MkDir()) + err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) + c.Assert(err, check.IsNil) + polkitCheckAuthorizationForPid = s.checkAuthorizationForPid +} + +func (s *daemonSuite) TearDownTest(c *check.C) { + dirs.SetRootDir("") + s.authorized = false + s.err = nil + logger.SetLogger(logger.NullLogger) +} + +func (s *daemonSuite) TearDownSuite(c *check.C) { + polkitCheckAuthorizationForPid = polkit.CheckAuthorizationForPid +} + +// build a new daemon, with only a little of Init(), suitable for the tests +func newTestDaemon(c *check.C) *Daemon { + d, err := New() + c.Assert(err, check.IsNil) + d.addRoutes() + + return d +} + +// a Response suitable for testing +type mockHandler struct { + cmd *Command + lastMethod string +} + +func (mck *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + mck.lastMethod = r.Method +} + +func mkRF(c *check.C, cmd *Command, mck *mockHandler) ResponseFunc { + return func(innerCmd *Command, req *http.Request, user *auth.UserState) Response { + c.Assert(cmd, check.Equals, innerCmd) + return mck + } +} + +func (s *daemonSuite) TestCommandMethodDispatch(c *check.C) { + cmd := &Command{d: newTestDaemon(c)} + mck := &mockHandler{cmd: cmd} + rf := mkRF(c, cmd, mck) + cmd.GET = rf + cmd.PUT = rf + cmd.POST = rf + cmd.DELETE = rf + + for _, method := range []string{"GET", "POST", "PUT", "DELETE"} { + req, err := http.NewRequest(method, "", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + cmd.ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 401, check.Commentf(method)) + + rec = httptest.NewRecorder() + req.RemoteAddr = "pid=100;uid=0;" + req.RemoteAddr + + cmd.ServeHTTP(rec, req) + c.Check(mck.lastMethod, check.Equals, method) + c.Check(rec.Code, check.Equals, 200) + } + + req, err := http.NewRequest("POTATO", "", nil) + c.Assert(err, check.IsNil) + req.RemoteAddr = "pid=100;uid=0;" + req.RemoteAddr + + rec := httptest.NewRecorder() + cmd.ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 405) +} + +func (s *daemonSuite) TestGuestAccess(c *check.C) { + get := &http.Request{Method: "GET"} + put := &http.Request{Method: "PUT"} + pst := &http.Request{Method: "POST"} + del := &http.Request{Method: "DELETE"} + + cmd := &Command{d: newTestDaemon(c)} + c.Check(cmd.canAccess(get, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(pst, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(del, nil), check.Equals, accessUnauthorized) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(pst, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(del, nil), check.Equals, accessUnauthorized) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(pst, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(del, nil), check.Equals, accessUnauthorized) + + // Since this request has no RemoteAddr, it must be coming from the snap + // socket instead of the snapd one. In that case, if SnapOK is true, this + // command should be wide open for all HTTP methods. + cmd = &Command{d: newTestDaemon(c), SnapOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(pst, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(del, nil), check.Equals, accessOK) +} + +func (s *daemonSuite) TestUserAccess(c *check.C) { + get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=42;"} + put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;"} + + cmd := &Command{d: newTestDaemon(c)} + c.Check(cmd.canAccess(get, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) + + // Since this request has a RemoteAddr, it must be coming from the snapd + // socket instead of the snap one. In that case, SnapOK should have no + // bearing on the default behavior, which is to deny access. + cmd = &Command{d: newTestDaemon(c), SnapOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessUnauthorized) + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) +} + +func (s *daemonSuite) TestSuperAccess(c *check.C) { + get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=0;"} + put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=0;"} + + cmd := &Command{d: newTestDaemon(c)} + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) + + cmd = &Command{d: newTestDaemon(c), SnapOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) +} + +func (s *daemonSuite) TestPolkitAccess(c *check.C) { + put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;"} + cmd := &Command{d: newTestDaemon(c), PolkitOK: "polkit.action"} + + // polkit says user is not authorised + s.authorized = false + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) + + // polkit grants authorisation + s.authorized = true + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) + + // an error occurs communicating with polkit + s.err = errors.New("error") + c.Check(cmd.canAccess(put, nil), check.Equals, accessUnauthorized) + + // if the user dismisses the auth request, forbid access + s.err = polkit.ErrDismissed + c.Check(cmd.canAccess(put, nil), check.Equals, accessForbidden) +} + +func (s *daemonSuite) TestPolkitAccessForGet(c *check.C) { + get := &http.Request{Method: "GET", RemoteAddr: "pid=100;uid=42;"} + cmd := &Command{d: newTestDaemon(c), PolkitOK: "polkit.action"} + + // polkit can grant authorisation for GET requests + s.authorized = true + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) + + // for UserOK commands, polkit is not consulted + cmd.UserOK = true + polkitCheckAuthorizationForPid = func(pid uint32, actionId string, details map[string]string, flags polkit.CheckFlags) (bool, error) { + panic("polkit.CheckAuthorizationForPid called") + } + c.Check(cmd.canAccess(get, nil), check.Equals, accessOK) +} + +func (s *daemonSuite) TestPolkitInteractivity(c *check.C) { + put := &http.Request{Method: "PUT", RemoteAddr: "pid=100;uid=42;", Header: make(http.Header)} + cmd := &Command{d: newTestDaemon(c), PolkitOK: "polkit.action"} + s.authorized = true + + var logbuf bytes.Buffer + log, err := logger.New(&logbuf, logger.DefaultFlags) + c.Assert(err, check.IsNil) + logger.SetLogger(log) + + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) + c.Check(s.lastPolkitFlags, check.Equals, polkit.CheckNone) + c.Check(logbuf.String(), check.Equals, "") + + put.Header.Set(client.AllowInteractionHeader, "true") + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) + c.Check(s.lastPolkitFlags, check.Equals, polkit.CheckAllowInteraction) + c.Check(logbuf.String(), check.Equals, "") + + // bad values are logged and treated as false + put.Header.Set(client.AllowInteractionHeader, "garbage") + c.Check(cmd.canAccess(put, nil), check.Equals, accessOK) + c.Check(s.lastPolkitFlags, check.Equals, polkit.CheckNone) + c.Check(logbuf.String(), testutil.Contains, "error parsing X-Allow-Interaction header:") +} + +func (s *daemonSuite) TestAddRoutes(c *check.C) { + d := newTestDaemon(c) + + expected := make([]string, len(api)) + for i, v := range api { + expected[i] = v.Path + } + + got := make([]string, 0, len(api)) + c.Assert(d.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + got = append(got, route.GetName()) + return nil + }), check.IsNil) + + c.Check(got, check.DeepEquals, expected) // this'll stop being true if routes are added that aren't commands (e.g. for the favicon) + + // XXX: still waiting to know how to check d.router.NotFoundHandler has been set to NotFound + // the old test relied on undefined behaviour: + // c.Check(fmt.Sprintf("%p", d.router.NotFoundHandler), check.Equals, fmt.Sprintf("%p", NotFound)) +} + +type witnessAcceptListener struct { + net.Listener + + accept chan struct{} + accept1 bool + + closed chan struct{} + closed1 bool + closedLck sync.Mutex +} + +func (l *witnessAcceptListener) Accept() (net.Conn, error) { + if !l.accept1 { + l.accept1 = true + close(l.accept) + } + return l.Listener.Accept() +} + +func (l *witnessAcceptListener) Close() error { + err := l.Listener.Close() + if l.closed != nil { + l.closedLck.Lock() + defer l.closedLck.Unlock() + if !l.closed1 { + l.closed1 = true + close(l.closed) + } + } + return err +} + +func (s *daemonSuite) markSeeded(d *Daemon) { + st := d.overlord.State() + st.Lock() + st.Set("seeded", true) + auth.SetDevice(st, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + Serial: "serialserial", + }) + st.Unlock() +} + +func (s *daemonSuite) TestStartStop(c *check.C) { + d := newTestDaemon(c) + // mark as already seeded + s.markSeeded(d) + + l, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, check.IsNil) + + snapdAccept := make(chan struct{}) + d.snapdListener = &witnessAcceptListener{Listener: l, accept: snapdAccept} + + snapAccept := make(chan struct{}) + d.snapListener = &witnessAcceptListener{Listener: l, accept: snapAccept} + + d.Start() + + snapdDone := make(chan struct{}) + go func() { + select { + case <-snapdAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapdDone) + }() + + snapDone := make(chan struct{}) + go func() { + select { + case <-snapAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapDone) + }() + + <-snapdDone + <-snapDone + + err = d.Stop() + c.Check(err, check.IsNil) +} + +func (s *daemonSuite) TestRestartWiring(c *check.C) { + d := newTestDaemon(c) + // mark as already seeded + s.markSeeded(d) + + l, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, check.IsNil) + + snapdAccept := make(chan struct{}) + d.snapdListener = &witnessAcceptListener{Listener: l, accept: snapdAccept} + + snapAccept := make(chan struct{}) + d.snapListener = &witnessAcceptListener{Listener: l, accept: snapAccept} + + d.Start() + defer d.Stop() + + snapdDone := make(chan struct{}) + go func() { + select { + case <-snapdAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapdDone) + }() + + snapDone := make(chan struct{}) + go func() { + select { + case <-snapAccept: + case <-time.After(2 * time.Second): + c.Fatal("snap accept was not called") + } + close(snapDone) + }() + + <-snapdDone + <-snapDone + + d.overlord.State().RequestRestart(state.RestartDaemon) + + select { + case <-d.Dying(): + case <-time.After(2 * time.Second): + c.Fatal("RequestRestart -> overlord -> Kill chain didn't work") + } +} + +func (s *daemonSuite) TestGracefulStop(c *check.C) { + d := newTestDaemon(c) + + responding := make(chan struct{}) + doRespond := make(chan bool, 1) + + d.router.HandleFunc("/endp", func(w http.ResponseWriter, r *http.Request) { + close(responding) + if <-doRespond { + w.Write([]byte("OKOK")) + } else { + w.Write([]byte("Gone")) + } + return + }) + + // mark as already seeded + s.markSeeded(d) + + snapdL, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, check.IsNil) + + snapL, err := net.Listen("tcp", "127.0.0.1:0") + c.Assert(err, check.IsNil) + + snapdAccept := make(chan struct{}) + snapdClosed := make(chan struct{}) + d.snapdListener = &witnessAcceptListener{Listener: snapdL, accept: snapdAccept, closed: snapdClosed} + + snapAccept := make(chan struct{}) + d.snapListener = &witnessAcceptListener{Listener: snapL, accept: snapAccept} + + d.Start() + + snapdAccepting := make(chan struct{}) + go func() { + select { + case <-snapdAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapdAccepting) + }() + + snapAccepting := make(chan struct{}) + go func() { + select { + case <-snapAccept: + case <-time.After(2 * time.Second): + c.Fatal("snapd accept was not called") + } + close(snapAccepting) + }() + + <-snapdAccepting + <-snapAccepting + + alright := make(chan struct{}) + + go func() { + res, err := http.Get(fmt.Sprintf("http://%s/endp", snapdL.Addr())) + c.Assert(err, check.IsNil) + c.Check(res.StatusCode, check.Equals, 200) + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + c.Assert(err, check.IsNil) + c.Check(string(body), check.Equals, "OKOK") + close(alright) + }() + go func() { + <-snapdClosed + time.Sleep(200 * time.Millisecond) + doRespond <- true + }() + + <-responding + err = d.Stop() + doRespond <- false + c.Check(err, check.IsNil) + + select { + case <-alright: + case <-time.After(2 * time.Second): + c.Fatal("never got proper response") + } +} diff --git a/daemon/response.go b/daemon/response.go new file mode 100644 index 00000000..882e2d07 --- /dev/null +++ b/daemon/response.go @@ -0,0 +1,346 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "mime" + "net/http" + "path/filepath" + "strconv" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/systemd" +) + +// ResponseType is the response type +type ResponseType string + +// "there are three standard return types: Standard return value, +// Background operation, Error", each returning a JSON object with the +// following "type" field: +const ( + ResponseTypeSync ResponseType = "sync" + ResponseTypeAsync ResponseType = "async" + ResponseTypeError ResponseType = "error" +) + +// Response knows how to serve itself, and how to find itself +type Response interface { + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +type resp struct { + Status int `json:"status-code"` + Type ResponseType `json:"type"` + Result interface{} `json:"result,omitempty"` + *Meta +} + +// TODO This is being done in a rush to get the proper external +// JSON representation in the API in time for the release. +// The right code style takes a bit more work and unifies +// these fields inside resp. +type Meta struct { + Sources []string `json:"sources,omitempty"` + Paging *Paging `json:"paging,omitempty"` + SuggestedCurrency string `json:"suggested-currency,omitempty"` + Change string `json:"change,omitempty"` +} + +type Paging struct { + Page int `json:"page"` + Pages int `json:"pages"` +} + +type respJSON struct { + Type ResponseType `json:"type"` + Status int `json:"status-code"` + StatusText string `json:"status"` + Result interface{} `json:"result"` + *Meta +} + +func (r *resp) MarshalJSON() ([]byte, error) { + return json.Marshal(respJSON{ + Type: r.Type, + Status: r.Status, + StatusText: http.StatusText(r.Status), + Result: r.Result, + Meta: r.Meta, + }) +} + +func (r *resp) ServeHTTP(w http.ResponseWriter, _ *http.Request) { + status := r.Status + bs, err := r.MarshalJSON() + if err != nil { + logger.Noticef("cannot marshal %#v to JSON: %v", *r, err) + bs = nil + status = 500 + } + + hdr := w.Header() + if r.Status == 202 || r.Status == 201 { + if m, ok := r.Result.(map[string]interface{}); ok { + if location, ok := m["resource"]; ok { + if location, ok := location.(string); ok && location != "" { + hdr.Set("Location", location) + } + } + } + } + + hdr.Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(bs) +} + +type errorKind string + +const ( + errorKindTwoFactorRequired = errorKind("two-factor-required") + errorKindTwoFactorFailed = errorKind("two-factor-failed") + errorKindLoginRequired = errorKind("login-required") + errorKindInvalidAuthData = errorKind("invalid-auth-data") + errorKindTermsNotAccepted = errorKind("terms-not-accepted") + errorKindNoPaymentMethods = errorKind("no-payment-methods") + errorKindPaymentDeclined = errorKind("payment-declined") + errorKindPasswordPolicy = errorKind("password-policy") + + errorKindSnapAlreadyInstalled = errorKind("snap-already-installed") + errorKindSnapNotInstalled = errorKind("snap-not-installed") + errorKindSnapNotFound = errorKind("snap-not-found") + errorKindAppNotFound = errorKind("app-not-found") + errorKindSnapLocal = errorKind("snap-local") + errorKindSnapNoUpdateAvailable = errorKind("snap-no-update-available") + + errorKindNotSnap = errorKind("snap-not-a-snap") + + errorKindSnapNeedsDevMode = errorKind("snap-needs-devmode") + errorKindSnapNeedsClassic = errorKind("snap-needs-classic") + errorKindSnapNeedsClassicSystem = errorKind("snap-needs-classic-system") +) + +type errorValue interface{} + +type errorResult struct { + Message string `json:"message"` // note no omitempty + Kind errorKind `json:"kind,omitempty"` + Value errorValue `json:"value,omitempty"` +} + +// SyncResponse builds a "sync" response from the given result. +func SyncResponse(result interface{}, meta *Meta) Response { + if err, ok := result.(error); ok { + return InternalError("internal error: %v", err) + } + + if rsp, ok := result.(Response); ok { + return rsp + } + + return &resp{ + Type: ResponseTypeSync, + Status: 200, + Result: result, + Meta: meta, + } +} + +// AsyncResponse builds an "async" response from the given *Task +func AsyncResponse(result map[string]interface{}, meta *Meta) Response { + return &resp{ + Type: ResponseTypeAsync, + Status: 202, + Result: result, + Meta: meta, + } +} + +// makeErrorResponder builds an errorResponder from the given error status. +func makeErrorResponder(status int) errorResponder { + return func(format string, v ...interface{}) Response { + res := &errorResult{ + Message: fmt.Sprintf(format, v...), + } + if status == 401 { + res.Kind = errorKindLoginRequired + } + return &resp{ + Type: ResponseTypeError, + Result: res, + Status: status, + } + } +} + +// A FileResponse 's ServeHTTP method serves the file +type FileResponse string + +// ServeHTTP from the Response interface +func (f FileResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { + filename := fmt.Sprintf("attachment; filename=%s", filepath.Base(string(f))) + w.Header().Add("Content-Disposition", filename) + http.ServeFile(w, r, string(f)) +} + +// A journalLineReaderSeqResponse's ServeHTTP method reads lines (presumed to +// be, each one on its own, a JSON dump of a systemd.Log, as output by +// journalctl -o json) from an io.ReadCloser, loads that into a client.Log, and +// outputs the json dump of that, padded with RS and LF to make it a valid +// json-seq response. +// +// The reader is always closed when done (this is important for +// osutil.WatingStdoutPipe). +// +// Tip: “jq” knows how to read this; “jq --seq” both reads and writes this. +type journalLineReaderSeqResponse struct { + io.ReadCloser + follow bool +} + +func (rr *journalLineReaderSeqResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json-seq") + + flusher, hasFlusher := w.(http.Flusher) + + var err error + dec := json.NewDecoder(rr) + writer := bufio.NewWriter(w) + enc := json.NewEncoder(writer) + for { + var log systemd.Log + if err = dec.Decode(&log); err != nil { + break + } + + writer.WriteByte(0x1E) // RS -- see ascii(7), and RFC7464 + + // ignore the error... + t, _ := log.Time() + if err = enc.Encode(client.Log{ + Timestamp: t, + Message: log.Message(), + SID: log.SID(), + PID: log.PID(), + }); err != nil { + break + } + + if rr.follow { + if e := writer.Flush(); e != nil { + break + } + if hasFlusher { + flusher.Flush() + } + } + } + if err != nil && err != io.EOF { + fmt.Fprintf(writer, `\x1E{"error": %q}\n`, err) + logger.Noticef("cannot stream response; problem reading: %v", err) + } + if err := writer.Flush(); err != nil { + logger.Noticef("cannot stream response; problem writing: %v", err) + } + rr.Close() +} + +type assertResponse struct { + assertions []asserts.Assertion + bundle bool +} + +// AssertResponse builds a response whose ServerHTTP method serves one or a bundle of assertions. +func AssertResponse(asserts []asserts.Assertion, bundle bool) Response { + if len(asserts) > 1 { + bundle = true + } + return &assertResponse{assertions: asserts, bundle: bundle} +} + +func (ar assertResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { + t := asserts.MediaType + if ar.bundle { + t = mime.FormatMediaType(t, map[string]string{"bundle": "y"}) + } + w.Header().Set("Content-Type", t) + w.Header().Set("X-Ubuntu-Assertions-Count", strconv.Itoa(len(ar.assertions))) + w.WriteHeader(200) + enc := asserts.NewEncoder(w) + for _, a := range ar.assertions { + err := enc.Encode(a) + if err != nil { + logger.Noticef("cannot write encoded assertion into response: %v", err) + break + + } + } +} + +// errorResponder is a callable that produces an error Response. +// e.g., InternalError("something broke: %v", err), etc. +type errorResponder func(string, ...interface{}) Response + +// standard error responses +var ( + Unauthorized = makeErrorResponder(401) + NotFound = makeErrorResponder(404) + BadRequest = makeErrorResponder(400) + MethodNotAllowed = makeErrorResponder(405) + InternalError = makeErrorResponder(500) + NotImplemented = makeErrorResponder(501) + Forbidden = makeErrorResponder(403) + Conflict = makeErrorResponder(409) +) + +// SnapNotFound is an error responder used when an operation is +// requested on a snap that doesn't exist. +func SnapNotFound(snapName string, err error) Response { + return &resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindSnapNotFound, + Value: snapName, + }, + Status: 404, + } +} + +// AppNotFound is an error responder used when an operation is +// requested on a app that doesn't exist. +func AppNotFound(format string, v ...interface{}) Response { + res := &errorResult{ + Message: fmt.Sprintf(format, v...), + Kind: errorKindAppNotFound, + } + return &resp{ + Type: ResponseTypeError, + Result: res, + Status: 404, + } +} diff --git a/daemon/response_test.go b/daemon/response_test.go new file mode 100644 index 00000000..bc5a40ba --- /dev/null +++ b/daemon/response_test.go @@ -0,0 +1,109 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + + "gopkg.in/check.v1" +) + +type responseSuite struct{} + +var _ = check.Suite(&responseSuite{}) + +func (s *responseSuite) TestRespSetsLocationIfAccepted(c *check.C) { + rec := httptest.NewRecorder() + + rsp := &resp{ + Status: 202, + Result: map[string]interface{}{ + "resource": "foo/bar", + }, + } + + rsp.ServeHTTP(rec, nil) + hdr := rec.Header() + c.Check(hdr.Get("Location"), check.Equals, "foo/bar") +} + +func (s *responseSuite) TestRespSetsLocationIfCreated(c *check.C) { + rec := httptest.NewRecorder() + + rsp := &resp{ + Status: 201, + Result: map[string]interface{}{ + "resource": "foo/bar", + }, + } + + rsp.ServeHTTP(rec, nil) + hdr := rec.Header() + c.Check(hdr.Get("Location"), check.Equals, "foo/bar") +} + +func (s *responseSuite) TestRespDoesNotSetLocationIfOther(c *check.C) { + rec := httptest.NewRecorder() + + rsp := &resp{ + Status: 418, // I'm a teapot + Result: map[string]interface{}{ + "resource": "foo/bar", + }, + } + + rsp.ServeHTTP(rec, nil) + hdr := rec.Header() + c.Check(hdr.Get("Location"), check.Equals, "") +} + +func (s *responseSuite) TestFileResponseSetsContentDisposition(c *check.C) { + const filename = "icon.png" + + path := filepath.Join(c.MkDir(), filename) + err := ioutil.WriteFile(path, nil, os.ModePerm) + c.Check(err, check.IsNil) + + rec := httptest.NewRecorder() + rsp := FileResponse(path) + req, err := http.NewRequest("GET", "", nil) + c.Check(err, check.IsNil) + + rsp.ServeHTTP(rec, req) + + hdr := rec.Header() + c.Check(hdr.Get("Content-Disposition"), check.Equals, + fmt.Sprintf("attachment; filename=%s", filename)) +} + +// Due to how the protocol was defined the result must be sent, even if it is +// null. Older clients rely on this. +func (s *responseSuite) TestRespJSONWithNullResult(c *check.C) { + rj := &respJSON{Result: nil} + data, err := json.Marshal(rj) + c.Assert(err, check.IsNil) + c.Check(string(data), check.Equals, `{"type":"","status-code":0,"status":"","result":null}`) +} diff --git a/daemon/snap.go b/daemon/snap.go new file mode 100644 index 00000000..f3715d6a --- /dev/null +++ b/daemon/snap.go @@ -0,0 +1,403 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/systemd" +) + +var errNoSnap = errors.New("snap not installed") + +// snapIcon tries to find the icon inside the snap +func snapIcon(info *snap.Info) string { + // XXX: copy of snap.Snap.Icon which will go away + found, _ := filepath.Glob(filepath.Join(info.MountDir(), "meta", "gui", "icon.*")) + if len(found) == 0 { + return info.IconURL + } + + return found[0] +} + +// snapDate returns the time of the snap mount directory. +func snapDate(info *snap.Info) time.Time { + st, err := os.Stat(info.MountDir()) + if err != nil { + return time.Time{} + } + + return st.ModTime() +} + +func publisherName(st *state.State, info *snap.Info) (string, error) { + if info.SnapID == "" { + return "", nil + } + + pubAcct, err := assertstate.Publisher(st, info.SnapID) + if err != nil { + return "", fmt.Errorf("cannot find publisher details: %v", err) + } + return pubAcct.Username(), nil +} + +type aboutSnap struct { + info *snap.Info + snapst *snapstate.SnapState + publisher string +} + +// localSnapInfo returns the information about the current snap for the given name plus the SnapState with the active flag and other snap revisions. +func localSnapInfo(st *state.State, name string) (aboutSnap, error) { + st.Lock() + defer st.Unlock() + + var snapst snapstate.SnapState + err := snapstate.Get(st, name, &snapst) + if err != nil && err != state.ErrNoState { + return aboutSnap{}, fmt.Errorf("cannot consult state: %v", err) + } + + info, err := snapst.CurrentInfo() + if err == snapstate.ErrNoCurrent { + return aboutSnap{}, errNoSnap + } + if err != nil { + return aboutSnap{}, fmt.Errorf("cannot read snap details: %v", err) + } + + publisher, err := publisherName(st, info) + if err != nil { + return aboutSnap{}, err + } + + return aboutSnap{ + info: info, + snapst: &snapst, + publisher: publisher, + }, nil +} + +// allLocalSnapInfos returns the information about the all current snaps and their SnapStates. +func allLocalSnapInfos(st *state.State, all bool, wanted map[string]bool) ([]aboutSnap, error) { + st.Lock() + defer st.Unlock() + + snapStates, err := snapstate.All(st) + if err != nil { + return nil, err + } + about := make([]aboutSnap, 0, len(snapStates)) + + var firstErr error + for name, snapst := range snapStates { + if len(wanted) > 0 && !wanted[name] { + continue + } + var aboutThis []aboutSnap + var info *snap.Info + var publisher string + var err error + if all { + for _, seq := range snapst.Sequence { + info, err = snap.ReadInfo(seq.RealName, seq) + if err != nil { + break + } + publisher, err = publisherName(st, info) + aboutThis = append(aboutThis, aboutSnap{info, snapst, publisher}) + } + } else { + info, err = snapst.CurrentInfo() + if err == nil { + var publisher string + publisher, err = publisherName(st, info) + aboutThis = append(aboutThis, aboutSnap{info, snapst, publisher}) + } + } + + if err != nil { + // XXX: aggregate instead? + if firstErr == nil { + firstErr = err + } + continue + } + about = append(about, aboutThis...) + } + + return about, firstErr +} + +type bySnapApp []*snap.AppInfo + +func (a bySnapApp) Len() int { return len(a) } +func (a bySnapApp) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a bySnapApp) Less(i, j int) bool { + iName := a[i].Snap.Name() + jName := a[j].Snap.Name() + if iName == jName { + return a[i].Name < a[j].Name + } + return iName < jName +} + +// this differs from snap.SplitSnapApp in the handling of the +// snap-only case: +// snap.SplitSnapApp("foo") is ("foo", "foo"), +// splitAppName("foo") is ("foo", ""). +func splitAppName(s string) (snap, app string) { + if idx := strings.IndexByte(s, '.'); idx > -1 { + return s[:idx], s[idx+1:] + } + + return s, "" +} + +type appInfoOptions struct { + service bool +} + +func (opts appInfoOptions) String() string { + if opts.service { + return "service" + } + + return "app" +} + +// appInfosFor returns a sorted list apps described by names. +// +// * If names is empty, returns all apps of the wanted kinds (which +// could be an empty list). +// * An element of names can be a snap name, in which case all apps +// from the snap of the wanted kind are included in the result (and +// it's an error if the snap has no apps of the wanted kind). +// * An element of names can instead be snap.app, in which case that app is +// included in the result (and it's an error if the snap and app don't +// both exist, or if the app is not a wanted kind) +// On error an appropriate error Response is returned; a nil Response means +// no error. +// +// It's a programming error to call this with wanted having neither +// services nor commands set. +func appInfosFor(st *state.State, names []string, opts appInfoOptions) ([]*snap.AppInfo, Response) { + snapNames := make(map[string]bool) + requested := make(map[string]bool) + for _, name := range names { + requested[name] = true + name, _ = splitAppName(name) + snapNames[name] = true + } + + snaps, err := allLocalSnapInfos(st, false, snapNames) + if err != nil { + return nil, InternalError("cannot list local snaps! %v", err) + } + + found := make(map[string]bool) + appInfos := make([]*snap.AppInfo, 0, len(requested)) + for _, snp := range snaps { + snapName := snp.info.Name() + apps := make([]*snap.AppInfo, 0, len(snp.info.Apps)) + for _, app := range snp.info.Apps { + if !opts.service || app.IsService() { + apps = append(apps, app) + } + } + + if len(apps) == 0 && requested[snapName] { + return nil, AppNotFound("snap %q has no %ss", snapName, opts) + } + + includeAll := len(requested) == 0 || requested[snapName] + if includeAll { + // want all services in a snap + found[snapName] = true + } + + for _, app := range apps { + appName := snapName + "." + app.Name + if includeAll || requested[appName] { + appInfos = append(appInfos, app) + found[appName] = true + } + } + } + + for k := range requested { + if !found[k] { + if snapNames[k] { + return nil, SnapNotFound(k, fmt.Errorf("snap %q not found", k)) + } else { + snap, app := splitAppName(k) + return nil, AppNotFound("snap %q has no %s %q", snap, opts, app) + } + } + } + + sort.Sort(bySnapApp(appInfos)) + + return appInfos, nil +} + +func clientAppInfosFromSnapAppInfos(apps []*snap.AppInfo) []client.AppInfo { + // TODO: pass in an actual notifier here instead of null + // (Status doesn't _need_ it, but benefits from it) + sysd := systemd.New(dirs.GlobalRootDir, progress.Null) + + out := make([]client.AppInfo, len(apps)) + for i, app := range apps { + out[i] = client.AppInfo{ + Snap: app.Snap.Name(), + Name: app.Name, + } + if fn := app.DesktopFile(); osutil.FileExists(fn) { + out[i].DesktopFile = fn + } + + if app.IsService() { + // TODO: look into making a single call to Status for all services + if sts, err := sysd.Status(app.ServiceName()); err != nil { + logger.Noticef("cannot get status of service %q: %v", app.Name, err) + } else if len(sts) != 1 { + logger.Noticef("cannot get status of service %q: expected 1 result, got %d", app.Name, len(sts)) + } else { + out[i].Daemon = sts[0].Daemon + out[i].Enabled = sts[0].Enabled + out[i].Active = sts[0].Active + } + } + } + + return out +} + +func mapLocal(about aboutSnap) *client.Snap { + localSnap, snapst := about.info, about.snapst + status := "installed" + if snapst.Active && localSnap.Revision == snapst.Current { + status = "active" + } + + snapapps := make([]*snap.AppInfo, 0, len(localSnap.Apps)) + for _, app := range localSnap.Apps { + snapapps = append(snapapps, app) + } + sort.Sort(bySnapApp(snapapps)) + + apps := clientAppInfosFromSnapAppInfos(snapapps) + + // TODO: expose aliases information and state? + + result := &client.Snap{ + Description: localSnap.Description(), + Developer: about.publisher, + Icon: snapIcon(localSnap), + ID: localSnap.SnapID, + InstallDate: snapDate(localSnap), + InstalledSize: localSnap.Size, + Name: localSnap.Name(), + Revision: localSnap.Revision, + Status: status, + Summary: localSnap.Summary(), + Type: string(localSnap.Type), + Version: localSnap.Version, + Channel: localSnap.Channel, + TrackingChannel: snapst.Channel, + IgnoreValidation: snapst.IgnoreValidation, + Confinement: string(localSnap.Confinement), + DevMode: snapst.DevMode, + TryMode: snapst.TryMode, + JailMode: snapst.JailMode, + Private: localSnap.Private, + Apps: apps, + Broken: localSnap.Broken, + Contact: localSnap.Contact, + Title: localSnap.Title(), + License: localSnap.License, + } + + return result +} + +func mapRemote(remoteSnap *snap.Info) *client.Snap { + status := "available" + if remoteSnap.MustBuy { + status = "priced" + } + + confinement := remoteSnap.Confinement + if confinement == "" { + confinement = snap.StrictConfinement + } + + screenshots := make([]client.Screenshot, len(remoteSnap.Screenshots)) + for i, screenshot := range remoteSnap.Screenshots { + screenshots[i] = client.Screenshot{ + URL: screenshot.URL, + Width: screenshot.Width, + Height: screenshot.Height, + } + } + + result := &client.Snap{ + Description: remoteSnap.Description(), + Developer: remoteSnap.Publisher, + DownloadSize: remoteSnap.Size, + Icon: snapIcon(remoteSnap), + ID: remoteSnap.SnapID, + Name: remoteSnap.Name(), + Revision: remoteSnap.Revision, + Status: status, + Summary: remoteSnap.Summary(), + Type: string(remoteSnap.Type), + Version: remoteSnap.Version, + Channel: remoteSnap.Channel, + Private: remoteSnap.Private, + Confinement: string(confinement), + Contact: remoteSnap.Contact, + Title: remoteSnap.Title(), + License: remoteSnap.License, + Screenshots: screenshots, + Prices: remoteSnap.Prices, + Channels: remoteSnap.Channels, + Tracks: remoteSnap.Tracks, + } + + return result +} diff --git a/daemon/ucrednet.go b/daemon/ucrednet.go new file mode 100644 index 00000000..f568eb50 --- /dev/null +++ b/daemon/ucrednet.go @@ -0,0 +1,113 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "errors" + "fmt" + "net" + "strconv" + "strings" + sys "syscall" +) + +var errNoID = errors.New("no pid/uid found") + +const ( + ucrednetNoProcess = uint32(0) + ucrednetNobody = uint32((1 << 32) - 1) +) + +func ucrednetGet(remoteAddr string) (pid uint32, uid uint32, err error) { + pid = ucrednetNoProcess + uid = ucrednetNobody + for _, token := range strings.Split(remoteAddr, ";") { + var v uint64 + if strings.HasPrefix(token, "pid=") { + if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil { + pid = uint32(v) + } else { + break + } + } else if strings.HasPrefix(token, "uid=") { + if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil { + uid = uint32(v) + } else { + break + } + } + } + if pid == ucrednetNoProcess || uid == ucrednetNobody { + err = errNoID + } + + return pid, uid, err +} + +type ucrednetAddr struct { + net.Addr + pid string + uid string +} + +func (wa *ucrednetAddr) String() string { + return fmt.Sprintf("pid=%s;uid=%s;%s", wa.pid, wa.uid, wa.Addr) +} + +type ucrednetConn struct { + net.Conn + pid string + uid string +} + +func (wc *ucrednetConn) RemoteAddr() net.Addr { + return &ucrednetAddr{wc.Conn.RemoteAddr(), wc.pid, wc.uid} +} + +type ucrednetListener struct{ net.Listener } + +var getUcred = sys.GetsockoptUcred + +func (wl *ucrednetListener) Accept() (net.Conn, error) { + con, err := wl.Listener.Accept() + if err != nil { + return nil, err + } + + var pid, uid string + if ucon, ok := con.(*net.UnixConn); ok { + f, err := ucon.File() + if err != nil { + return nil, err + } + // File() is a dup(); needs closing + defer f.Close() + + ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED) + if err != nil { + return nil, err + } + + pid = strconv.FormatUint(uint64(ucred.Pid), 10) + uid = strconv.FormatUint(uint64(ucred.Uid), 10) + } + + return &ucrednetConn{con, pid, uid}, err +} diff --git a/daemon/ucrednet_test.go b/daemon/ucrednet_test.go new file mode 100644 index 00000000..9919b390 --- /dev/null +++ b/daemon/ucrednet_test.go @@ -0,0 +1,179 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package daemon + +import ( + "errors" + "net" + "path/filepath" + sys "syscall" + + "gopkg.in/check.v1" +) + +type ucrednetSuite struct { + ucred *sys.Ucred + err error +} + +var _ = check.Suite(&ucrednetSuite{}) + +func (s *ucrednetSuite) getUcred(fd, level, opt int) (*sys.Ucred, error) { + return s.ucred, s.err +} + +func (s *ucrednetSuite) SetUpSuite(c *check.C) { + getUcred = s.getUcred +} + +func (s *ucrednetSuite) TearDownTest(c *check.C) { + s.ucred = nil + s.err = nil +} +func (s *ucrednetSuite) TearDownSuite(c *check.C) { + getUcred = sys.GetsockoptUcred +} + +func (s *ucrednetSuite) TestAcceptConnRemoteAddrString(c *check.C) { + s.ucred = &sys.Ucred{Pid: 100, Uid: 42} + d := c.MkDir() + sock := filepath.Join(d, "sock") + + l, err := net.Listen("unix", sock) + c.Assert(err, check.IsNil) + defer l.Close() + + go func() { + cli, err := net.Dial("unix", sock) + c.Assert(err, check.IsNil) + cli.Close() + }() + + wl := &ucrednetListener{l} + + conn, err := wl.Accept() + c.Assert(err, check.IsNil) + defer conn.Close() + + remoteAddr := conn.RemoteAddr().String() + c.Check(remoteAddr, check.Matches, "pid=100;uid=42;.*") + pid, uid, err := ucrednetGet(remoteAddr) + c.Check(pid, check.Equals, uint32(100)) + c.Check(uid, check.Equals, uint32(42)) + c.Check(err, check.IsNil) +} + +func (s *ucrednetSuite) TestNonUnix(c *check.C) { + l, err := net.Listen("tcp", "localhost:0") + c.Assert(err, check.IsNil) + defer l.Close() + + addr := l.Addr().String() + + go func() { + cli, err := net.Dial("tcp", addr) + c.Assert(err, check.IsNil) + cli.Close() + }() + + wl := &ucrednetListener{l} + + conn, err := wl.Accept() + c.Assert(err, check.IsNil) + defer conn.Close() + + remoteAddr := conn.RemoteAddr().String() + c.Check(remoteAddr, check.Matches, "pid=;uid=;.*") + pid, uid, err := ucrednetGet(remoteAddr) + c.Check(pid, check.Equals, ucrednetNoProcess) + c.Check(uid, check.Equals, ucrednetNobody) + c.Check(err, check.Equals, errNoID) +} + +func (s *ucrednetSuite) TestAcceptErrors(c *check.C) { + s.ucred = &sys.Ucred{Pid: 100, Uid: 42} + d := c.MkDir() + sock := filepath.Join(d, "sock") + + l, err := net.Listen("unix", sock) + c.Assert(err, check.IsNil) + c.Assert(l.Close(), check.IsNil) + + wl := &ucrednetListener{l} + + _, err = wl.Accept() + c.Assert(err, check.NotNil) +} + +func (s *ucrednetSuite) TestUcredErrors(c *check.C) { + s.err = errors.New("oopsie") + d := c.MkDir() + sock := filepath.Join(d, "sock") + + l, err := net.Listen("unix", sock) + c.Assert(err, check.IsNil) + defer l.Close() + + go func() { + cli, err := net.Dial("unix", sock) + c.Assert(err, check.IsNil) + cli.Close() + }() + + wl := &ucrednetListener{l} + + _, err = wl.Accept() + c.Assert(err, check.Equals, s.err) +} + +func (s *ucrednetSuite) TestGetNoUid(c *check.C) { + pid, uid, err := ucrednetGet("pid=100;uid=;") + c.Check(err, check.Equals, errNoID) + c.Check(pid, check.Equals, uint32(100)) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetBadUid(c *check.C) { + pid, uid, err := ucrednetGet("pid=100;uid=hello;") + c.Check(err, check.NotNil) + c.Check(pid, check.Equals, uint32(100)) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetNonUcrednet(c *check.C) { + pid, uid, err := ucrednetGet("hello") + c.Check(err, check.Equals, errNoID) + c.Check(pid, check.Equals, ucrednetNoProcess) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetNothing(c *check.C) { + pid, uid, err := ucrednetGet("") + c.Check(err, check.Equals, errNoID) + c.Check(pid, check.Equals, ucrednetNoProcess) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGet(c *check.C) { + pid, uid, err := ucrednetGet("pid=100;uid=42;") + c.Check(err, check.IsNil) + c.Check(pid, check.Equals, uint32(100)) + c.Check(uid, check.Equals, uint32(42)) +} diff --git a/data/Makefile b/data/Makefile new file mode 100644 index 00000000..b4703223 --- /dev/null +++ b/data/Makefile @@ -0,0 +1,4 @@ +all install clean: + $(MAKE) -C systemd $@ + $(MAKE) -C dbus $@ + $(MAKE) -C env $@ diff --git a/data/completion/complete.sh b/data/completion/complete.sh new file mode 100644 index 00000000..a8934293 --- /dev/null +++ b/data/completion/complete.sh @@ -0,0 +1,129 @@ +# -*- bash -*- +# +# Copyright (C) 2017 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# 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 . + +# _complete_from_snap performs the tab completion request by calling the +# appropriate 'snap run --command=complete' with serialized args, and +# deserializes the response into the usual tab completion result. +# +# How snap command completion works is: +# 1. snapd's complete.sh is sourced into the user's shell environment +# 2. user performs ' '. If '' is a snap command, +# proceed to step '3', otherwise perform normal bash completion +# 3. run 'snap run --command=complete ...', converting bash completion +# environment into serialized command line arguments +# 4. 'snap run --command=complete ...' exec()s 'etelpmoc.sh' within the snap's +# runtime environment and confinement +# 5. 'etelpmoc.sh' takes the serialized command line arguments from step '3' +# and puts them back into the bash completion environment variables +# 6. 'etelpmoc.sh' sources the snap's 'completer' script, performs the bash +# completion and serializes the resulting completion environment variables +# by printing to stdout the results in a format that snapd's complete.sh +# will understand, then exits +# 7. control returns to snapd's 'complete.sh' and it deserializes the output +# from 'etelpmoc.sh', validates the results and puts the validated results +# into the bash completion environment variables +# 8. bash displays the results to the user +type -t _complete_from_snap > /dev/null || +_complete_from_snap() { + { + # De-serialize the output of 'snap run --command=complete ...' into the format + # bash expects: + read -r -a opts + # opts is expected to be a series of compopt options + if [[ ${#opts[@]} -gt 0 ]]; then + if [[ "${opts[0]}" == "cannot" ]]; then + # older snap-execs sent errors over stdout :-( + return 1 + fi + + for i in "${opts[@]}"; do + if ! [[ "$i" =~ ^[a-z]+$ ]]; then + # only lowercase alpha characters allowed + return 2 + fi + done + fi + + read -r bounced + case "$bounced" in + ""|"alias"|"export"|"job"|"variable") + # OK + ;; + *) + # unrecognised bounce + return 2 + ;; + esac + + read -r sep + if [ -n "$sep" ]; then + # non-blank separator? madness! + return 2 + fi + local oldIFS="$IFS" + + if [ ! "$bounced" ]; then + local IFS=$'\n' + # Ignore any suspicious results that are uncommon in filenames and that + # might be used to trick the user. A whitelist approach would be better + # but is impractical with UTF-8 and common characters like quotes. + COMPREPLY=( $( command grep -v '[[:cntrl:];&?*{}]' ) ) + IFS="$oldIFS" + fi + + if [[ ${#opts[@]} -gt 0 ]]; then + # shellcheck disable=SC2046 + # (we *want* word splitting to happen here) + compopt $(printf " -o %s" "${opts[@]}") + fi + if [ "$bounced" ]; then + # We validated '$bounced' above and '${COMP_WORDS[$COMP_CWORD]}' is + # coming from the user's session, not the snap so skip input + # validation: we aren't trying to protect the user from themselves. + COMPREPLY+=(compgen -A "$bounced" -- "${COMP_WORDS[$COMP_CWORD]}") + fi + } < <( + snap run --command=complete "$1" "$COMP_TYPE" "$COMP_KEY" "$COMP_POINT" "$COMP_CWORD" "$COMP_WORDBREAKS" "$COMP_LINE" "${COMP_WORDS[@]}" 2>/dev/null || return 1 + ) + +} + +# this file can be sourced directly as e.g. /usr/lib/snapd/complete.sh, or via +# a symlink from /usr/share/bash-completion/completions/. In the first case we +# want to load the default loader; in the second, the specific one. +# +if [[ "${BASH_SOURCE[0]}" =~ ^/usr/share/bash-completion/completions/ ]]; then + complete -F _complete_from_snap "$1" +else + + # _complete_from_snap_maybe calls _complete_from_snap if the command is in + # bin/snap, and otherwise does bash-completion's _completion_loader (which is + # what -D would've done before). + type -t _complete_from_snap_maybe > /dev/null || + _complete_from_snap_maybe() { + local etel=snap/core/current/usr/lib/snapd/etelpmoc.sh + # catch /snap/bin and /var/lib/snapd/snap/bin + if [[ "$(which "$1")" =~ /snap/bin/ && ( -e "/var/lib/snapd/$etel" || -e "/$etel" ) ]]; then + complete -F _complete_from_snap "$1" + return 124 + fi + # fallback to the old -D + _completion_loader "$1" + } + + complete -D -F _complete_from_snap_maybe +fi + diff --git a/data/completion/etelpmoc.sh b/data/completion/etelpmoc.sh new file mode 100755 index 00000000..24e28d74 --- /dev/null +++ b/data/completion/etelpmoc.sh @@ -0,0 +1,212 @@ +#!/bin/bash +# +# Copyright (C) 2017 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# 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 . + +# etelpmoc is the reverse of complete: it de-serialises the tab completion +# request into the appropriate environment variables expected by the tab +# completion tools, performs whatever action is wanted, and serialises the +# result. It accomplishes this by having functions override the builtin +# completion commands. +# +# this always runs "inside", in the same environment you get when doing "snap +# run --shell", and snap-exec is the one setting the first argument to the +# completion script set in the snap. The rest of the arguments come through +# from snap-run --command=complete + +_die() { + echo "$*" >&2 + exit 1 +} + +if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + _die "ERROR: this is meant to be run, not sourced." +fi + +if [[ "${#@}" -lt 8 ]]; then + _die "USAGE: $0 diff --git a/tests/lib/snaps/test-snapd-python-webserver/server.py b/tests/lib/snaps/test-snapd-python-webserver/server.py new file mode 100755 index 00000000..d19fc217 --- /dev/null +++ b/tests/lib/snaps/test-snapd-python-webserver/server.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 + +import os +import sys +import urllib.request + +from http.server import HTTPServer, SimpleHTTPRequestHandler + + +class XkcdRequestHandler(SimpleHTTPRequestHandler): + + XKCD_URL = "http://xkcd.com/" + XKCD_IMG_URL = "http://imgs.xkcd.com/" + + def _mini_proxy(self, url): + fp = urllib.request.urlopen(url) + body = fp.read() + info = fp.info() + self.send_response(200, "ok") + for k, v in info.items(): + self.send_header(k, v) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if self.path.startswith("/xkcd/"): + url = self.XKCD_URL + self.path[len("/xkcd/"):] + return self._mini_proxy(url) + elif self.path.startswith("/img/xkcd/"): + url = self.XKCD_IMG_URL + self.path[len("/img/xkcd/"):] + return self._mini_proxy(url) + else: + return super(XkcdRequestHandler, self).do_GET() + + +if __name__ == "__main__": + # we start in the snappy base directory, ensure we are in "www" + os.chdir(os.path.dirname(__file__) + "/../www") + + if len(sys.argv) > 1: + port = int(sys.argv[1]) + else: + port = 80 + + httpd = HTTPServer(('', port), XkcdRequestHandler) + httpd.serve_forever() diff --git a/tests/lib/snaps/test-snapd-python-webserver/snapcraft.yaml b/tests/lib/snaps/test-snapd-python-webserver/snapcraft.yaml new file mode 100644 index 00000000..4488ff2d --- /dev/null +++ b/tests/lib/snaps/test-snapd-python-webserver/snapcraft.yaml @@ -0,0 +1,21 @@ +name: test-snapd-python-webserver +version: 16.04-3 +summary: Python based example webserver +description: | + Show random XKCD comic via a build-in webserver + This is meant as a fun example for a snappy package. +apps: + test-snapd-python-webserver: + command: bin/test-snapd-python-webserver + daemon: simple + plugs: [network, network-bind] + +parts: + test-snapd-python-webserver: + plugin: python3 + copy: + plugin: dump + source: . + organize: + server.py: bin/test-snapd-python-webserver + index.html: www/index.html diff --git a/tests/lib/snaps/test-snapd-requires-base/meta/snap.yaml b/tests/lib/snaps/test-snapd-requires-base/meta/snap.yaml new file mode 100644 index 00000000..0042f8a6 --- /dev/null +++ b/tests/lib/snaps/test-snapd-requires-base/meta/snap.yaml @@ -0,0 +1,4 @@ +name: test-snapd-requires-base +base: test-snapd-base +version: 1.0 +summary: A test snap that requires a specific base snap diff --git a/tests/lib/snaps/test-snapd-service-try-v1/bin/service b/tests/lib/snaps/test-snapd-service-try-v1/bin/service new file mode 100755 index 00000000..d07a9f95 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service-try-v1/bin/service @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "service v1" diff --git a/tests/lib/snaps/test-snapd-service-try-v1/meta/snap.yaml b/tests/lib/snaps/test-snapd-service-try-v1/meta/snap.yaml new file mode 100644 index 00000000..621add0e --- /dev/null +++ b/tests/lib/snaps/test-snapd-service-try-v1/meta/snap.yaml @@ -0,0 +1,5 @@ +name: test-snapd-service-try +version: 1.0 +apps: + service: + command: bin/service diff --git a/tests/lib/snaps/test-snapd-service-try-v2/bin/service b/tests/lib/snaps/test-snapd-service-try-v2/bin/service new file mode 100755 index 00000000..d07a9f95 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service-try-v2/bin/service @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "service v1" diff --git a/tests/lib/snaps/test-snapd-service-try-v2/meta/snap.yaml b/tests/lib/snaps/test-snapd-service-try-v2/meta/snap.yaml new file mode 100644 index 00000000..c663c917 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service-try-v2/meta/snap.yaml @@ -0,0 +1,6 @@ +name: test-snapd-service-try +version: 1.0 +apps: + service: + command: bin/service + daemon: simple diff --git a/tests/lib/snaps/test-snapd-service-v1-good/bin/good b/tests/lib/snaps/test-snapd-service-v1-good/bin/good new file mode 100755 index 00000000..d07a9f95 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service-v1-good/bin/good @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "service v1" diff --git a/tests/lib/snaps/test-snapd-service-v1-good/meta/snap.yaml b/tests/lib/snaps/test-snapd-service-v1-good/meta/snap.yaml new file mode 100644 index 00000000..89c39ba2 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service-v1-good/meta/snap.yaml @@ -0,0 +1,7 @@ +name: test-snapd-service +version: 1.0 +apps: + service: + command: bin/good + daemon: oneshot + restart-condition: never diff --git a/tests/lib/snaps/test-snapd-service-v2-bad/bin/bad b/tests/lib/snaps/test-snapd-service-v2-bad/bin/bad new file mode 100755 index 00000000..1229b388 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service-v2-bad/bin/bad @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "service v2" +exit 1 diff --git a/tests/lib/snaps/test-snapd-service-v2-bad/meta/snap.yaml b/tests/lib/snaps/test-snapd-service-v2-bad/meta/snap.yaml new file mode 100644 index 00000000..5d2dcaba --- /dev/null +++ b/tests/lib/snaps/test-snapd-service-v2-bad/meta/snap.yaml @@ -0,0 +1,7 @@ +name: test-snapd-service +version: 2.0 +apps: + service: + command: bin/bad + daemon: oneshot + restart-condition: never diff --git a/tests/lib/snaps/test-snapd-service/bin/reload b/tests/lib/snaps/test-snapd-service/bin/reload new file mode 100755 index 00000000..edaaf5aa --- /dev/null +++ b/tests/lib/snaps/test-snapd-service/bin/reload @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "reloading reloading reloading" diff --git a/tests/lib/snaps/test-snapd-service/bin/start b/tests/lib/snaps/test-snapd-service/bin/start new file mode 100755 index 00000000..067e111b --- /dev/null +++ b/tests/lib/snaps/test-snapd-service/bin/start @@ -0,0 +1,6 @@ +#!/bin/sh +snapctl get service-option > "$SNAP_DATA/service-option" +while true; do + echo "running" + sleep 10 +done diff --git a/tests/lib/snaps/test-snapd-service/bin/start-other b/tests/lib/snaps/test-snapd-service/bin/start-other new file mode 100755 index 00000000..067e111b --- /dev/null +++ b/tests/lib/snaps/test-snapd-service/bin/start-other @@ -0,0 +1,6 @@ +#!/bin/sh +snapctl get service-option > "$SNAP_DATA/service-option" +while true; do + echo "running" + sleep 10 +done diff --git a/tests/lib/snaps/test-snapd-service/meta/hooks/configure b/tests/lib/snaps/test-snapd-service/meta/hooks/configure new file mode 100755 index 00000000..ba28bdfd --- /dev/null +++ b/tests/lib/snaps/test-snapd-service/meta/hooks/configure @@ -0,0 +1,19 @@ +#!/bin/sh + +snapctl set service-option="$(snapctl get service-option-source)" + +COMMAND=$(snapctl get command) +if [ "$COMMAND" != "" ]; then + if [ "$COMMAND" = "restart" ]; then + snapctl restart test-snapd-service.test-snapd-service + else + snapctl "$COMMAND" test-snapd-service.test-snapd-service + fi +fi + +snapctl restart test-snapd-service.test-snapd-other-service + +# We need to sleep a little bit, otherwise the restarts in the test happen to quickly +# and systemd fails them with: +# `Start request repeated too quickly.` error in the journal (making the tests fail). +sleep 3 diff --git a/tests/lib/snaps/test-snapd-service/meta/snap.yaml b/tests/lib/snaps/test-snapd-service/meta/snap.yaml new file mode 100644 index 00000000..85196454 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service/meta/snap.yaml @@ -0,0 +1,10 @@ +name: test-snapd-service +version: 1.0 +apps: + test-snapd-service: + command: bin/start + daemon: simple + reload-command: bin/reload + test-snapd-other-service: + command: bin/start-other + daemon: simple diff --git a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml new file mode 100644 index 00000000..750a3913 --- /dev/null +++ b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml @@ -0,0 +1,9 @@ +name: test-snapd-sh +summary: A no-strings-attached, no-fuss shell for writing tests +version: 1.0 +apps: + test-snapd-sh: + command: ../../../bin/sh + with-home-plug: + command: ../../../bin/sh + plugs: [home] diff --git a/tests/lib/snaps/test-snapd-system-observe-consumer/consumer.py b/tests/lib/snaps/test-snapd-system-observe-consumer/consumer.py new file mode 100755 index 00000000..2a7e7bab --- /dev/null +++ b/tests/lib/snaps/test-snapd-system-observe-consumer/consumer.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +import os +import sys + +def run(): + with open('/proc/tty/drivers', 'r') as f: + print(f.read()) + +if __name__ == '__main__': + sys.exit(run()) diff --git a/tests/lib/snaps/test-snapd-system-observe-consumer/dbus-introspect.py b/tests/lib/snaps/test-snapd-system-observe-consumer/dbus-introspect.py new file mode 100755 index 00000000..68e91423 --- /dev/null +++ b/tests/lib/snaps/test-snapd-system-observe-consumer/dbus-introspect.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +import dbus +import sys + +def run(): + obj = dbus.SystemBus().get_object("org.freedesktop.hostname1", "/org/freedesktop/hostname1") + print(obj.Introspect(dbus_interface="org.freedesktop.DBus.Introspectable")) + +if __name__ == "__main__": + sys.exit(run()) diff --git a/tests/lib/snaps/test-snapd-system-observe-consumer/snapcraft.yaml b/tests/lib/snaps/test-snapd-system-observe-consumer/snapcraft.yaml new file mode 100644 index 00000000..8cbc2381 --- /dev/null +++ b/tests/lib/snaps/test-snapd-system-observe-consumer/snapcraft.yaml @@ -0,0 +1,25 @@ +name: test-snapd-system-observe-consumer +version: 1.0 +summary: Basic system-observe consumer snap +description: A basic snap declaring a plug on system-observe +grade: stable +confinement: strict + +apps: + dbus-introspect: + plugs: [system-observe] + command: bin/dbus-introspect + consumer: + plugs: [system-observe] + command: bin/consumer + +parts: + consumer: + plugin: python + stage-packages: [python3-dbus] + copy: + plugin: dump + source: . + organize: + dbus-introspect.py: bin/dbus-introspect + consumer.py: bin/consumer diff --git a/tests/lib/snaps/test-snapd-tools/bin/block b/tests/lib/snaps/test-snapd-tools/bin/block new file mode 100755 index 00000000..2380b826 --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/bin/block @@ -0,0 +1,14 @@ +#!/bin/sh + +set -ex + +cd "$SNAP" +echo "blocking dir $(pwd)" + +# add a marker so that applications that rely on this blocker +# can detect when its ready +touch "$SNAP_DATA/block-running" + +echo "waiting, press ctrl-c to stop" +sleep 999999 +exit 0 diff --git a/tests/lib/snaps/test-snapd-tools/bin/cat b/tests/lib/snaps/test-snapd-tools/bin/cat new file mode 100755 index 00000000..add6287f --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/bin/cat @@ -0,0 +1,3 @@ +#!/bin/sh + +cat "$@" diff --git a/tests/lib/snaps/test-snapd-tools/bin/cmd b/tests/lib/snaps/test-snapd-tools/bin/cmd new file mode 100755 index 00000000..7f3ed070 --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/bin/cmd @@ -0,0 +1,6 @@ +#!/bin/sh + +command="$1" +shift + +"$command" "$@" diff --git a/tests/lib/snaps/test-snapd-tools/bin/echo b/tests/lib/snaps/test-snapd-tools/bin/echo new file mode 100755 index 00000000..86fa5e54 --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/bin/echo @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "$@" diff --git a/tests/lib/snaps/test-snapd-tools/bin/env b/tests/lib/snaps/test-snapd-tools/bin/env new file mode 100755 index 00000000..5b465929 --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/bin/env @@ -0,0 +1,3 @@ +#!/bin/sh + +/usr/bin/env diff --git a/tests/lib/snaps/test-snapd-tools/bin/fail b/tests/lib/snaps/test-snapd-tools/bin/fail new file mode 100755 index 00000000..2bb8d868 --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/bin/fail @@ -0,0 +1,3 @@ +#!/bin/sh + +exit 1 diff --git a/tests/lib/snaps/test-snapd-tools/bin/head b/tests/lib/snaps/test-snapd-tools/bin/head new file mode 100755 index 00000000..0c2f9b4b --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/bin/head @@ -0,0 +1,3 @@ +#!/bin/sh + +head "$@" diff --git a/tests/lib/snaps/test-snapd-tools/bin/sh b/tests/lib/snaps/test-snapd-tools/bin/sh new file mode 100755 index 00000000..b7cb3d95 --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/bin/sh @@ -0,0 +1,13 @@ +#!/bin/bash + +cat <7Znw4ShQ%-o0XN7?XEE*BI4SVDN~+gj5R)11|Xt^ix)5c zC_O#hK@>$W*8UQWMk9Ln?oG>=FOLll4xZB0`M8Hnu20>b&vz2hfcBnOR8%aBh=|yF z_Uu^)qtSRr@&F(R0`l_m*rrXJCZ9cfmL6#X5RrA$i35KrSvoqWX6x!%#fyV7TF%7( zoroN69#>pktcs71M_5=G+}zw?G#cCeo+L@Iv9ZDU@#A4SP&6=_{gC0$Z-`W%{8g|mjLcZeG5#{COh>D8BtFOL_s;Vl) z#l^v3Fm#w7?gc_O68AV7eQ49NVe=+emsH|#pWc``aw&RUI|O}2378;j?iAg;(x$#`$|q(6R8kd+6H;~-ZmP+wAp$X&(o z4VsBpMizs+5C>Ch>u=t`ILLbShE?E;sEgSE@I2U<*$=(!3inBV=Erm1-*Gi1Lk1#n z9Cu5Y^TC#e4}5=^cDnT-E0x`XS0Tu4<_ zRV)$7+pXCJW#^X%Er_{tCS3-)dl^nTat@!Sxq(|73yZ#kQ1j&)fTT8KY*Z?vpT9pu zQEcP>3JWDh3=YJYu^})R3~in<#t;$`lEN74k``d?5ckT;%Gz95SU9$&r3Ef7E+$V; z&ow=J_WbbXasS$~x9aNo47H8g7O9En(5lt7o0ud?LJyTSg6F*e&C`8RR#FOECuex% zUx0A%3y2Lh0LOz&M%YXWD^S1w$%v?^sLx_!W2cl=lwthn7hqj2Am!gl=&5mqYoHgB zb1xz{=Q^fNomw6q9=_Dl((<48Erp55rlO)^Rd#mv%DTF`HtsA*68iV=Z**~SdCu0> z_6Gnsv~_>gp7qh{)|OU`oi`E1*#*eGd<`H17y}W3=XnfzZYYB0Kaaw~!Zu~gWHQ0U z!xJtEk&u!TVA8d~X3A?t>ZO16V2qKjQM>AYKZ{;>bXzRs7K-2AGn(K##1Ek>XJz_6 z>F?Jqhas(2J2fpWZGUxjb%*6ek|YcsJUB!Z~-#uG3-d=3(KC7`aN z22Prun7?ruuH{_o@S8*gFVEhv-}V+1K2H`IC#@KmaVqu7*Y9t3x9T5A_#(jPmXSGaCCRJ?NSoV&(C*qa&m%3 zqk$wz9Zn{b$>8DPfmXd<5Jkg{6_O-@I2q>5AC9-ZGtsc{cNlnN6MP(cfcTCJjmcz! zhld9&t*me^Pj}jM^(-I|5vr=HKt%BL z^hAAqJ!)%fZ^Y}bv8-0B!Sg)w^76n7Jh)%d2XO+1jrK)o-#RqLuSKg|31(phea1QP z7bfEAmzSZyF9hYytzb80{a-u@*4Ea@&CUJ!x5@#9S96h_as|U)8U=&ifQ&PjU^1C* zc#=^xqTdi-c=~zc_}-&18BGYB5On`#kTHgWf&%DtI{5kdK_-)dF$P;(TX=bSp}f2t zWo2b;5}9)^H9lT31R+@)(3rRfkhfK7%)$yShrWXPFH`ZXR0*X_00B@a6!7)+MP+3r z^78V)7;6(MN-GPbet!;W|4G4w*Fw?D+Z~KCSlL)(+KSmI$|}J3-zGpZN%!5*V5zC8 zRV5`Q>RTTul}dPdc_A}1v)yDEGBOAia|1vIedi~W$>I9d1^kenfmWUNwh&WW+hgmJ zPeHfn3L?UsHH#3x?Eq@at8bIj&rS%!;tg+cUC{z}6hb(cqokp_(usBpN#2J<$N`F*6|d9t4WtQnD&$ zo$utO)0+*tj+3iVG{S3OU#yGUv|{qI8C`A?2#hfiz(ykaEApl(Ie<3NZLkd&wmf5u=tI*K@Vj>AOw47 zv?L#askQ<#cW?3z=$8$^Iy3q5f|L6ax1anj5pt!XjoNha>V=S5FYXMPJ>eb3SXs9u zLB?1yfLDlUV|se}_R`YQfs!OarBcD$+xshbclQPN5_pVpAiWmNr{f@Z^M-2D0<>nF zhaoQ$vR+D#p8bnz1u)wgF zMx`xS_xeJ{SSsc`fGWTkI}ad$i00PS)xB+JXBWg6tGjJn58DPN$kb#MJBV?B@dCuM zd>D#zpz!jC!=gV!o05p8#NA+m{AcnVAaK&CIYjh&VOCy}#>ewx#@NoV50`bCVBVD+ zUuR9vN=sFbntQ4OjInR*?d=D)M;-uFFV5-ju{S$h;T-^UqMkEefT17@)Z7SN+9@zW zehiO4x|XM9$~OMkSru(E(m zA-|)VG>S$X+kK?p5BrWK=3L4?*zFf1jIrHzv%;M;yDqO09Njt<6N0llG*Rciu%ETk zzl(xq_>VuN+CD#fLZ{@iOd-egcjn>Hm-{C_u&Xh_PQ6wWou+pAX#alR&hhrLNd(5r z&~xqX1o!R94l-BIH|}S?%;d{RiaUt`&kjPs$RRL_x3}5#x>ihhEfh9tTg0r}j+Tbz z?hAoCctbsa-%BRbp!#puZ>x)0Ka@xU<9XQ4Tyf2A_R2Yo#k*nSX_$#g@sJ=8x)5YhaW({b;~z5F)Gd-d7ejo%Fo z3z)V~H2ij-AR0vU8|(v@K5mdJ6*ztHI1H`&Hr?+2Px?Jr4`^qXz6S91!5+YotlAaMeJ*W-MfcD&I9D;ulF!vKu2 z1AjZZIik<+`ZwLp2qX~{777G~2InvT`+HBZ$IYcnt{27p`Q?=}bS*j#032K$QC(7b zBM`EVg)?3>3nt{bcC)X52_FV6Q5r8h?{q*>k*Y1PE#l$DaPmWY)2Z$Wiw z)#!w%gID6d-1jXJdB6-a%rL_YGt4l<3^Q)v{{Z_Sg0D002ovPDHLkV1i_+ BUitt4 literal 0 HcmV?d00001 diff --git a/tests/lib/snaps/test-snapd-tools/meta/snap.yaml b/tests/lib/snaps/test-snapd-tools/meta/snap.yaml new file mode 100644 index 00000000..d527e5f9 --- /dev/null +++ b/tests/lib/snaps/test-snapd-tools/meta/snap.yaml @@ -0,0 +1,27 @@ +name: test-snapd-tools +version: 1.0 +environment: + EXTRA_GLOBAL: extra-global + EXTRA_CACHE_DIR: $SNAP_USER_DATA/.cache +apps: + echo: + command: bin/echo + success: + command: bin/success + fail: + command: bin/fail + block: + command: bin/block + cat: + command: bin/cat + head: + command: bin/head + env: + command: bin/env + environment: + EXTRA_LOCAL: extra-local + EXTRA_LOCAL_NESTED: ${EXTRA_GLOBAL}-nested + sh: + command: bin/sh + cmd: + command: bin/cmd diff --git a/tests/lib/snaps/test-snapd-tuntap/bin/tuntap.py b/tests/lib/snaps/test-snapd-tuntap/bin/tuntap.py new file mode 100755 index 00000000..4176e0ff --- /dev/null +++ b/tests/lib/snaps/test-snapd-tuntap/bin/tuntap.py @@ -0,0 +1,79 @@ +#!/usr/bin/python3 + +import contextlib +import os +import fcntl +import re +import struct +import sys +from typing import Iterable + + +def if_open(dev: str) -> int: + TUNSETIFF = 0x400454ca + TUNSETOWNER = TUNSETIFF + 2 + IFF_TUN = 0x0001 + IFF_TAP = 0x0002 + IFF_NO_PI = 0x1000 + + try: + fd = os.open("/dev/net/tun", os.O_RDWR) + except PermissionError as e: + raise SystemExit(e) + + if_flags = None + if dev.startswith('tun'): + if_flags = IFF_TUN | IFF_NO_PI + elif dev.startswith('tap'): + if_flags = IFF_TAP | IFF_NO_PI + + fcntl.ioctl(fd, TUNSETIFF, struct.pack("16sH", str.encode(dev), if_flags)) + + # for setting to owner + # fcntl.ioctl(fd, TUNSETOWNER, 1000) + + return fd + + +def device_exists(dev: str) -> bool: + return os.path.exists("/sys/devices/virtual/net/%s" % dev) + + +def valid_device_name(dev: str) -> None: + if not re.search(r'^t(ap|un)[0-9]+$', dev): + raise ValueError("device should be of form tun0-tun255 or tap0-tap255") + + if_num = int(dev[3:]) + if if_num > 255: + raise ValueError("device should be of form tun0-tun255 or tap0-tap255") + + +@contextlib.contextmanager +def closing_fd(fd: int) -> Iterable[int]: + try: + yield fd + finally: + os.close(fd) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + raise SystemExit("need to specify a tun/tap device (eg tun1 or tap2)") + + d = sys.argv[1] + valid_device_name(d) + + if device_exists(d): + raise SystemExit("ERROR: device '%s' already exists" % d) + sys.exit(1) + + found = False + with closing_fd(if_open(d)) as fd: + if device_exists(d): + found = True + print("PASS") + else: + print("FAIL") + + if not found: + sys.exit(1) diff --git a/tests/lib/snaps/test-snapd-tuntap/meta/snap.yaml b/tests/lib/snaps/test-snapd-tuntap/meta/snap.yaml new file mode 100644 index 00000000..fd4f408d --- /dev/null +++ b/tests/lib/snaps/test-snapd-tuntap/meta/snap.yaml @@ -0,0 +1,10 @@ +name: test-snapd-tuntap +version: 1.0 +summary: Test policy app for allocating tun/tap devices +description: Test policy app for allocating tun/tap devices +confinement: strict + +apps: + tuntap: + command: bin/tuntap.py + plugs: [ network-control ] diff --git a/tests/lib/snaps/test-snapd-uhid/Makefile b/tests/lib/snaps/test-snapd-uhid/Makefile new file mode 100644 index 00000000..4fcee237 --- /dev/null +++ b/tests/lib/snaps/test-snapd-uhid/Makefile @@ -0,0 +1,5 @@ +CFLAGS += -Wall -Werror +.PHONY: all +all: uhid-test + +uhid-test: uhid-test.c \ No newline at end of file diff --git a/tests/lib/snaps/test-snapd-uhid/snapcraft.yaml b/tests/lib/snaps/test-snapd-uhid/snapcraft.yaml new file mode 100644 index 00000000..8ab270ba --- /dev/null +++ b/tests/lib/snaps/test-snapd-uhid/snapcraft.yaml @@ -0,0 +1,17 @@ +name: test-snapd-uhid +version: 1.0 +summary: Basic snap declaring a plug on the uhid interface +description: Basic snap declaring a plug on the uhid interface +grade: stable +confinement: strict + +apps: + test-device: + command: uhid-test + plugs: [uhid] + +parts: + test: + source: . + plugin: make + artifacts: [uhid-test] diff --git a/tests/lib/snaps/test-snapd-uhid/uhid-test.c b/tests/lib/snaps/test-snapd-uhid/uhid-test.c new file mode 100644 index 00000000..5aa97184 --- /dev/null +++ b/tests/lib/snaps/test-snapd-uhid/uhid-test.c @@ -0,0 +1,190 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * For more information about the whole uhid example please read: + * https://elixir.free-electrons.com/linux/latest/source/samples/uhid/uhid-example.c + * + * HID Report Desciptor + * We emulate a basic 3 button mouse with wheel and 3 keyboard LEDs. This is + * the report-descriptor as the kernel will parse it: + * + * INPUT(1)[INPUT] + * Field(0) + * Physical(GenericDesktop.Pointer) + * Application(GenericDesktop.Mouse) + * Usage(3) + * Button.0001 + * Button.0002 + * Button.0003 + * Logical Minimum(0) + * Logical Maximum(1) + * Report Size(1) + * Report Count(3) + * Report Offset(0) + * Flags( Variable Absolute ) + * Field(1) + * Physical(GenericDesktop.Pointer) + * Application(GenericDesktop.Mouse) + * Usage(3) + * GenericDesktop.X + * GenericDesktop.Y + * GenericDesktop.Wheel + * Logical Minimum(-128) + * Logical Maximum(127) + * Report Size(8) + * Report Count(3) + * Report Offset(8) + * Flags( Variable Relative ) + * OUTPUT(2)[OUTPUT] + * Field(0) + * Application(GenericDesktop.Keyboard) + * Usage(3) + * LED.NumLock + * LED.CapsLock + * LED.ScrollLock + * Logical Minimum(0) + * Logical Maximum(1) + * Report Size(1) + * Report Count(3) + * Report Offset(0) + * Flags( Variable Absolute ) + * + * This is the mapping that we expect: + * Button.0001 ---> Key.LeftBtn + * Button.0002 ---> Key.RightBtn + * Button.0003 ---> Key.MiddleBtn + * GenericDesktop.X ---> Relative.X + * GenericDesktop.Y ---> Relative.Y + * GenericDesktop.Wheel ---> Relative.Wheel + * LED.NumLock ---> LED.NumLock + * LED.CapsLock ---> LED.CapsLock + * LED.ScrollLock ---> LED.ScrollLock + * + * This information can be verified by reading /sys/kernel/debug/hid//rdesc + * This file should print the same information as showed above. + */ + +static unsigned char rdesc[] = { + 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ + 0x09, 0x02, /* USAGE (Mouse) */ + 0xa1, 0x01, /* COLLECTION (Application) */ + 0x09, 0x01, /* USAGE (Pointer) */ + 0xa1, 0x00, /* COLLECTION (Physical) */ + 0x85, 0x01, /* REPORT_ID (1) */ + 0x05, 0x09, /* USAGE_PAGE (Button) */ + 0x19, 0x01, /* USAGE_MINIMUM (Button 1) */ + 0x29, 0x03, /* USAGE_MAXIMUM (Button 3) */ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ + 0x95, 0x03, /* REPORT_COUNT (3) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x81, 0x02, /* INPUT (Data,Var,Abs) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x05, /* REPORT_SIZE (5) */ + 0x81, 0x01, /* INPUT (Cnst,Var,Abs) */ + 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ + 0x09, 0x30, /* USAGE (X) */ + 0x09, 0x31, /* USAGE (Y) */ + 0x09, 0x38, /* USAGE (WHEEL) */ + 0x15, 0x81, /* LOGICAL_MINIMUM (-127) */ + 0x25, 0x7f, /* LOGICAL_MAXIMUM (127) */ + 0x75, 0x08, /* REPORT_SIZE (8) */ + 0x95, 0x03, /* REPORT_COUNT (3) */ + 0x81, 0x06, /* INPUT (Data,Var,Rel) */ + 0xc0, /* END_COLLECTION */ + 0xc0, /* END_COLLECTION */ + 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ + 0x09, 0x06, /* USAGE (Keyboard) */ + 0xa1, 0x01, /* COLLECTION (Application) */ + 0x85, 0x02, /* REPORT_ID (2) */ + 0x05, 0x08, /* USAGE_PAGE (Led) */ + 0x19, 0x01, /* USAGE_MINIMUM (1) */ + 0x29, 0x03, /* USAGE_MAXIMUM (3) */ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ + 0x95, 0x03, /* REPORT_COUNT (3) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x91, 0x02, /* Output (Data,Var,Abs) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x05, /* REPORT_SIZE (5) */ + 0x91, 0x01, /* Output (Cnst,Var,Abs) */ + 0xc0, /* END_COLLECTION */ +}; + +static int uhid_write(int fd, const struct uhid_event* ev) +{ + ssize_t ret; + + ret = write(fd, ev, sizeof(*ev)); + if (ret < 0) { + fprintf(stderr, "Cannot write to uhid: %m\n"); + return -1; + } else if (ret != sizeof(*ev)) { + fprintf(stderr, "Wrong size written to uhid: %ld != %lu\n", + ret, sizeof(*ev)); + return -1; + } + return 0; +} + +static int create(int fd) +{ + struct uhid_event ev = { + .type = UHID_CREATE, + .u = {.create.rd_data = rdesc, + .create.rd_size = sizeof(rdesc), + .create.bus = BUS_USB, + .create.vendor = 0x15d9, + .create.product = 0x0a37, + .create.version = 0, + .create.country = 0 } + + }; + strcpy((char*)ev.u.create.name, "test-uhid-device"); + + return uhid_write(fd, &ev); +} + +static void destroy(int fd) +{ + struct uhid_event ev; + + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_DESTROY; + + uhid_write(fd, &ev); +} + +int main(int argc, char** argv) +{ + int fd; + const char* path = "/dev/uhid"; + int ret; + + printf("Open uhid-cdev %s\n", path); + fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + fprintf(stderr, "Cannot open uhid-cdev %s: %m\n", path); + return EXIT_FAILURE; + } + + printf("Create uhid device\n"); + ret = create(fd); + if (ret != 0) { + close(fd); + return EXIT_FAILURE; + } + + printf("Destroy uhid device\n"); + destroy(fd); + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/tests/lib/snaps/test-snapd-unknown-interfaces/meta/snap.yaml b/tests/lib/snaps/test-snapd-unknown-interfaces/meta/snap.yaml new file mode 100644 index 00000000..cddcc6e4 --- /dev/null +++ b/tests/lib/snaps/test-snapd-unknown-interfaces/meta/snap.yaml @@ -0,0 +1,10 @@ +name: test-snapd-unknown-interfaces +summary: A snap with unknown plus and slot interfaces. +version: 1.0 +apps: + test-snapd-unknown-interfaces: + command: ../../../bin/sh +plugs: + bogus-plug: +slots: + bogus-slot: diff --git a/tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml b/tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml new file mode 100644 index 00000000..61db7f28 --- /dev/null +++ b/tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml @@ -0,0 +1,14 @@ +name: test-snapd-upower-observe-consumer +version: 1.0 +summary: Basic upower-observe consumer snap +description: A basic snap declaring a plug on upower-observe + +apps: + upower: + command: upower + plugs: [upower-observe] + +parts: + upower: + plugin: nil + stage-packages: [upower] diff --git a/tests/lib/snaps/test-snapd-with-configure/meta/hooks/configure b/tests/lib/snaps/test-snapd-with-configure/meta/hooks/configure new file mode 100755 index 00000000..039e4d00 --- /dev/null +++ b/tests/lib/snaps/test-snapd-with-configure/meta/hooks/configure @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/tests/lib/snaps/test-snapd-with-configure/meta/snap.yaml b/tests/lib/snaps/test-snapd-with-configure/meta/snap.yaml new file mode 100644 index 00000000..f153694d --- /dev/null +++ b/tests/lib/snaps/test-snapd-with-configure/meta/snap.yaml @@ -0,0 +1,4 @@ +name: test-snapd-with-configure +version: 1.0 +summary: Basic snap with a configure hook +description: A basic snap with a passthrough configure hook diff --git a/tests/lib/snaps/test-strict-cgroup/bin/read-fb b/tests/lib/snaps/test-strict-cgroup/bin/read-fb new file mode 100755 index 00000000..75207a3e --- /dev/null +++ b/tests/lib/snaps/test-strict-cgroup/bin/read-fb @@ -0,0 +1,3 @@ +#!/bin/sh +set -e +cat /dev/fb0 diff --git a/tests/lib/snaps/test-strict-cgroup/bin/read-kmsg b/tests/lib/snaps/test-strict-cgroup/bin/read-kmsg new file mode 100755 index 00000000..7f3d3db7 --- /dev/null +++ b/tests/lib/snaps/test-strict-cgroup/bin/read-kmsg @@ -0,0 +1,3 @@ +#!/bin/sh +set -e +head -1 /dev/kmsg diff --git a/tests/lib/snaps/test-strict-cgroup/meta/snap.yaml b/tests/lib/snaps/test-strict-cgroup/meta/snap.yaml new file mode 100644 index 00000000..58b6a94b --- /dev/null +++ b/tests/lib/snaps/test-strict-cgroup/meta/snap.yaml @@ -0,0 +1,14 @@ +name: test-strict-cgroup +version: 1.0 +summary: Basic strict cgroup tester +description: A basic snap declaring a plug to a udev tagged interface + (framebuffer) +confinement: strict + +apps: + read-fb: + command: bin/read-fb + plugs: [framebuffer] + read-kmsg: + command: bin/read-kmsg + plugs: [framebuffer] diff --git a/tests/lib/snaps/time-control-consumer/bin/read b/tests/lib/snaps/time-control-consumer/bin/read new file mode 100755 index 00000000..1cae6352 --- /dev/null +++ b/tests/lib/snaps/time-control-consumer/bin/read @@ -0,0 +1,2 @@ +#!/bin/sh +exec /sbin/hwclock -r -f /dev/rtc diff --git a/tests/lib/snaps/time-control-consumer/bin/timedatectl b/tests/lib/snaps/time-control-consumer/bin/timedatectl new file mode 100755 index 00000000..640cdecb --- /dev/null +++ b/tests/lib/snaps/time-control-consumer/bin/timedatectl @@ -0,0 +1,2 @@ +#!/bin/sh +exec /usr/bin/timedatectl "$@" diff --git a/tests/lib/snaps/time-control-consumer/bin/write b/tests/lib/snaps/time-control-consumer/bin/write new file mode 100755 index 00000000..3eb67abd --- /dev/null +++ b/tests/lib/snaps/time-control-consumer/bin/write @@ -0,0 +1,2 @@ +#!/bin/sh +exec /sbin/hwclock --systohc -f /dev/rtc diff --git a/tests/lib/snaps/time-control-consumer/meta/snap.yaml b/tests/lib/snaps/time-control-consumer/meta/snap.yaml new file mode 100644 index 00000000..ad472aba --- /dev/null +++ b/tests/lib/snaps/time-control-consumer/meta/snap.yaml @@ -0,0 +1,15 @@ +name: time-control-consumer +version: 1.0 +summary: Basic time-control consumer snap +description: A basic snap declaring a plug on a time-control slot + +apps: + read: + command: bin/read + plugs: [time-control] + write: + command: bin/write + plugs: [time-control] + timedatectl: + command: bin/timedatectl + plugs: [time-control] diff --git a/tests/lib/store.sh b/tests/lib/store.sh new file mode 100644 index 00000000..3915b3e9 --- /dev/null +++ b/tests/lib/store.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +STORE_CONFIG=/etc/systemd/system/snapd.service.d/store.conf + +# shellcheck source=tests/lib/systemd.sh +. "$TESTSLIB/systemd.sh" + +_configure_store_backends(){ + systemctl stop snapd.service snapd.socket + mkdir -p "$(dirname $STORE_CONFIG)" + rm -f "$STORE_CONFIG" + cat > "$STORE_CONFIG" <. + * + */ + +package main + +import ( + "fmt" + "os" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/systemd" +) + +var opts struct { + Path bool `short:"p" long:"path" description:"When escaping/unescaping assume the string is a path"` +} + +func main() { + args, err := flags.ParseArgs(&opts, os.Args) + if err != nil { + os.Exit(1) + } + + if !opts.Path { + panic("cannot use this systemd-escape without --path") + } + + for i, arg := range args[1:] { + fmt.Printf(systemd.EscapeUnitNamePath(arg)) + if i < len(args[1:])-1 { + fmt.Printf(" ") + } + } + fmt.Printf("\n") +} diff --git a/tests/lib/systemd.sh b/tests/lib/systemd.sh new file mode 100644 index 00000000..01cde183 --- /dev/null +++ b/tests/lib/systemd.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Use like systemd_create_and_start_unit(fakestore, "$(which fakestore) -start -dir $top_dir -addr localhost:11028 $@") +systemd_create_and_start_unit() { + printf "[Unit]\nDescription=For testing purposes\n[Service]\nType=simple\nExecStart=%s\n" "$2" > "/run/systemd/system/$1.service" + if [ -n "${3:-}" ]; then + echo "Environment=$3" >> "/run/systemd/system/$1.service" + fi + systemctl daemon-reload + systemctl start "$1" +} + +# Use like systemd_stop_and_destroy_unit(fakestore) +systemd_stop_and_destroy_unit() { + if systemctl is-active "$1"; then + systemctl stop "$1" + fi + rm -f "/run/systemd/system/$1.service" + systemctl daemon-reload +} + +wait_for_service() { + local service_name="$1" + local state="${2:-active}" + for i in $(seq 300); do + if systemctl show -p ActiveState "$service_name" | grep -q "ActiveState=$state"; then + return + fi + # show debug output every 1min + if [ $(( i % 60 )) = 0 ]; then + systemctl status "$service_name" || true; + fi + sleep 1; + done + + echo "service $service_name did not start" + exit 1 +} diff --git a/tests/main/abort/task.yaml b/tests/main/abort/task.yaml new file mode 100644 index 00000000..b8907ff0 --- /dev/null +++ b/tests/main/abort/task.yaml @@ -0,0 +1,42 @@ +summary: Check change abort + +environment: + SNAP_NAME: test-snapd-tools + +execute: | + . "$TESTSLIB/dirs.sh" + + echo "Abort with invalid id" + if snap abort 10000000; then + echo "abort with invalid id should fail" + exit 1 + fi + + echo "====================================" + + echo "Abort with valid id - error" + subdirPath="$SNAP_MOUNT_DIR/$SNAP_NAME/current/foo" + mkdir -p "$subdirPath" + . "$TESTSLIB/snaps.sh" + if install_local "$SNAP_NAME"; then + echo "install should fail when the target directory exists" + exit 1 + fi + idPattern="\d+(?= +Error.*?Install \"$SNAP_NAME\" snap)" + id=$(snap changes | grep -Pzo "$idPattern") + if snap abort "$id"; then + echo "abort with valid failed id should fail" + exit 1 + fi + rm -rf "$subdirPath" + + echo "====================================" + + echo "Abort with valid id - done" + install_local "$SNAP_NAME" + idPattern="\d+(?= +Done.*?Install \"$SNAP_NAME\" snap)" + id=$(snap changes | grep -Pzo "$idPattern") + if snap abort "$id"; then + echo "abort with valid done id should fail" + exit 1 + fi diff --git a/tests/main/ack/alice.account b/tests/main/ack/alice.account new file mode 100644 index 00000000..0c1ed311 --- /dev/null +++ b/tests/main/ack/alice.account @@ -0,0 +1,19 @@ +type: account +authority-id: testrootorg +account-id: BGLTY1rcRKQQMbt9B407lDH38lbCW3wg +display-name: Alice +timestamp: 2016-09-23T09:03:41Z +username: alice +validation: unproven +sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y + +AcLBUgQAAQoABgUCV+Tv7QAACZ8QAA1rmxGr9MP5K//OKDcEPWE848xPq0ZW61g8JWR0Xq1aaUd6 +mzdCzt6eJJjiOnlIhO6Orf1lmGliISF6MXEebXoF+2GX2tm+qAvqh3p7j4DTSk0Dqv0P1ficJknf +r+2CnA/BPEGKsnuvcPgc0JYLc+xah4R8JSrCks1d01j4cqTqso0MDACZW/bOWiGZfgnF/dzxO7iR +RIQHGPehw1OvnGfF+92BirdLfnTSmexvKLIMco7x37YZbYSgUzuWLgwsHic8xLs2jyOjiyP5IWX+ +2hJZlplLV+RyR+wud7+h2GVvoKizILeehb4eaMu1zabjDJX7ANG0svvB2FCWmJaUuclUitLvm5GC +HJTuB6J12DokGADIGdTNbQD6FCsgcCaDLMyVgxMvteIiBDWY6uvll/rFNSpdxPhkq535q178fKX1 +7kdRK4+QxsChNwHTKMj4mnLC2sg6SlGoKLXnr5OFanDbm/8yMVCXeH7qaWpE0+aVfAo/tXCTbl+G +m6DDZQ3IGbZpjrCi2O5ocmbyZfEbfcqJlGYV7cr4PKOa9NtplAjv64VD7P3l5oV4XIVgHOIcB2kc +HfmyJm2AywmTOsN4qqWcKC4hXFza3w6XfJer5uGdi9SyYn7nhFp/sUCWiBKk/s+nemxrP211bfvI +2weiS2RdvDKDhHbVbfIjcGJuI/7v diff --git a/tests/main/ack/alice.account-key b/tests/main/ack/alice.account-key new file mode 100644 index 00000000..1306f292 --- /dev/null +++ b/tests/main/ack/alice.account-key @@ -0,0 +1,31 @@ +type: account-key +authority-id: testrootorg +public-key-sha3-384: s2I2irs5PDzHx8n_-lEjkVUn81dvKujEmUiS0c3vwPbwojxDT_QUZ6ejDavhj_yU +account-id: BGLTY1rcRKQQMbt9B407lDH38lbCW3wg +name: default +since: 2016-09-23T11:03:52+02:00 +timestamp: 2016-09-23T09:03:41Z +body-length: 717 +sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y + +AcbBTQRWhcGAARAA0zc2RM3X30jS651jIn+94BXlljGnvFPxYboFpyu0pkAjTFPsrbfepQ0ZxrAa +T/I70Exjz3bYDJDPTak///qhiZA+KulaclYjOpleOCKhEVRCssRDqU54it/tbUU33LYD0ef0n1bj +FUBbe9qWUd9FFNSd4MDe6vCU++7xtpyNgEF/OCeJx3kTCvgYQAMY7z8id340Z0TvjH4YCMTonZb9 +78ed7fh4HnR0m40cGxR+gIC3R/n2MS7Bu1+N6xxHdXwP2agW1gggRthnbhTnNcd3hYgBgHQ4h+bC +asUlJUM8rB37gfXq6WgEzncLqt/Oh7YhJmPQGqZAhsyRzSm7RVDQuxULirXhz+7JEGSRzE0f9A0v +95vYl0UqQIdqENDj0DYJgVEH8gQ9nYCzENSKlxMpiEQFxwEwoSqqJK+znc/qye1TdGR204IPiky6 +IeySvrM7hhZjzPu6ZsQDqQZQbFZW+WGtiFFrRFhnlzIh/BMv6Hz2ybkZLkK/gD/H7XTAnnMOOITA +6CqX7/ZF79phW8iOdnp7AgO+0n5GAoSoUH55r/GZVhfbED20nd6yiXOs6Efoaq1M9Naue7SHbn71 +4bc2QvUIDOLnALr/SD2s6k9bTzYAJnmNCU0pRBbxtSaKUfhoK5m/ALYAvCoWHS+NJl455xO/Ovtf +/BAVo+oAJL+ByscAEQEAAQ== + +AcLBUgQAAQoABgUCV+Tv+AAAj0kQAF598DsZ3lE+qSWTdiGtktvihFmoMIq+m/4l8l+wf+26cdT2 +FStjSFLCfq3isg67vUAIG0pKL5F3i0l+YKQRrpBnIqDBPPZL/HnEo4XFlA/jSJAA5eLwHXBBZ82+ +0+04eR+9hvy6YVXuhTR2Cyv9+7WHw66SHp4+aIFC1/+J0QhqO3xH22jHkmJDnLj5K6JeoViLh3B0 +olTMBtWCfshvrd4QosU2V5gP0tz3qxOQe3cyLVpKOceNf/vCB7BYqynJF66TjNZ1khFye5LSjxFm +V+f/gIZ2HqzzbpVNsvw49RQ4TNhdN8IwZ4bDZeLc0icm2GkjBHGsSZCIfM7tPHSO5XZt+F61Z5lu +i72GS69AbTeuPwtSE2XTJkdFWyF2VlhJv64jv5E+Yr9PhX5HxT1VKwUw1eJ2CFRTEujM4rHMSQJ0 +H4a9VN7jOsUR/eJt4pS6GteZIZp525tsNNPNWisBq94WYrbjEkkrqFy9oBUW9cPO5vcIJ0mXrCQi +z2fUCf25Az/GAdfmrm2fFBW4doecY5M3Fs0Q8YtCpLtmGoevM4bqOk+P0Ko9sQdlNo7yDrBTewGX +J2Ie/9/iCdOFLcV0Mo9amvbmSq5qlkTgvBneY3XVfwbLHaVjNHUK0USpk0pYF3i8VY2ltz0w5iKH +dwJ5d4nyG7WUSh8gnn4Wh2IVnLYF diff --git a/tests/main/ack/bob.assertions b/tests/main/ack/bob.assertions new file mode 100644 index 00000000..63e232ba --- /dev/null +++ b/tests/main/ack/bob.assertions @@ -0,0 +1,51 @@ +type: account-key +authority-id: testrootorg +public-key-sha3-384: kW5sfrKZI2rIAT70JkttRq2VlNa9t8EHOoWrL2ZBAa7tLWZMy2KBweZEh3_MLcZh +account-id: ct1P6H12NnpJ1nj2jxNX94lHp6sHClxT +name: default +since: 2016-09-23T11:09:18+02:00 +timestamp: 2016-09-23T09:09:04Z +body-length: 717 +sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y + +AcbBTQRWhcGAARAA6ruGAKWOS3ESwR2UwkcqEWJZ+8JjZM+PDuLVjXv6+Bu+vByfyhuOxlScPZs2 +vexsU+HQOvXjTVXBDSvSMNyMxq1SUSUTlqDOOC+F6ZGXfbmRV9zG1YVXn9L2YK7DRSvQ4PywLP4C +qRbSMcsnjxIgYmXgd1tY70Y01DACn9anVQW2PqZjh1Ite4FLHq8YTgsI9sUQFfpqTitCEZqKt7+D +EAru5yufXxWi1d6cRjUJCRHZqRpGU0HRVNDe6va66mrYJ9LT8q16merOVhOaS1btuIbRGHbvBwEL +d3eDk3Do6HiG8oUZ3jrM0J7dqhMBstSA+9YbpMIQZBA4YF+SYOVaMN2X74yUrAuRmcExPFyh1enF +HA5cNqhK3Cgga8EnHivXhdFoPOJBQwkSOJY1JP/EvJ+V/4hlQHreZgxwwhWfET2mpN46zpnUT3zI +53pyvQZZl1Heg84gh41fLMAeURrEo5IUDR4IBva5QiN7h/lyhsNzTKjaQaM8lognVUL80hm9UgFQ +GGwrvxrhZjj7WsbO10ei2p3Z6SEsDihrIbmIJl49fc4KzRcQB+l3TllheLgAVrUJrOaomWTrrrKN +Iz2maNYX1tmOwGwhQQZr4uo7ve+qC3MOCRHHkCEJpWoHYDyb1cPOhIngwqgq9TjVAO6wx/iJTEGJ +XAwsu/h4eprPytcAEQEAAQ== + +AcLBUgQAAQoABgUCV+TxPgAANGcQADjczRIpexAoQwUfXyz65CmWflIYJIRBr7jFzC2ZIa1uZtDA +bSF4j1bCrFkSAXjfwmJYmEdBkQeC3H9LV2yPfwj/vxkyic0b7yptrriLBufYS1KbhrBN/gYETDYw +tGOTSZGlQqZkCiU/nwJz/evG7XmQiL65g4lhwoboilHNvDo2Rhq/An4Sc0Gvlk+UJx1KM5eTWkIO +7vxQzD9PbsUR/aCvSGTZ5HD1g4rDkWNpVXkXGrME9icp/IqlDlhU/tvAeMkITZlOesd4ENPzn/8W +lSuxVuwQ0FB+URl+6m/sMkh3p9GA51q3uvE931bXXSElMw455b2B/Idy+UDZEhYFlunAtBAFxRnE +mz8VG0PchJ5ozBjVG3hf/2Pe9KFHRPhJDWPrlBwo29oc1X93N1xf3vxVI/lQ1HIWXJAYtBuGj8RV +eX04kPA3piGt1uD4dGVu1FhSvDvnuelkFeBYG7b5uotlAmhW4THCUEGKmeZ/XDLY7980dWYIRduK +VCHY4pjLBf7KFJ34+EGKdNh3nxqISEoi/sQHyPI+21rLYSzuFTgsUQB3KwE2GlSeAWMzr6PXWn1e +yADnvpX9bzJJIErGP5PzxyDHPVfXO8eGIdcl2Nj/dSKujSVyq0tYVxjjOBcwxY/AcgYy+D1mpsH3 +/6cJP2vThDIA304fJF0FDLEuU6aa + +type: account +authority-id: testrootorg +account-id: ct1P6H12NnpJ1nj2jxNX94lHp6sHClxT +display-name: Bob +timestamp: 2016-09-23T09:09:04Z +username: bob +validation: unproven +sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y + +AcLBUgQAAQoABgUCV+TxMAAAa/sQAIoPmlkgThWTaBsQEODgQQCATq3EcFfKz3goeiAgW80cb3G/ +GYcKMUJMfyWAAJtW5wWPBI0XgSSf43bJ8k2S5DNW0xLvFCbgonUjmQ0glRa4st4Pe/DzSpTwitVW +G5m6Ls8Tr7Tbz+5tKxTZ+/VZnuqhAInHwp1WocMWDtFCPQD/u+KPkuK6a1rpeIIRXjw7uteHmRVK +dLba2cVN7m9sfGMtkIi9mUOPqabupei6ulVioz0IYZbRR8tWmo4BQ9Qwk5m0S6iRcNe9hKcddrgZ +W1jCcH3IHtk1nGsssezKIbqucw7W2wMSmWI3LiwzD2R1GPUNC/yb/UP8eJZuo7U39gx9zHJmvXO9 +4ZiJht72bgqrnypgpUBH/rrJM50MlcUjBd6PO3fAMX3h36UT85rj3VS7KLL41R73lySStiXZ9qX/ +URqmFVeqo26izDjqW/Ab6+yI3212N+Gk2MpiqmD8n5kwpR3kVJKma788KaeoqkbgSZCEp3CIuDry +btHPpbLwenw3LIGR1LZyNWJ+WKiJ/JKecK6xGMkzMpv4xX5SGPvacNwBHoeosSUqsWXxXM93e7Pk +3LHg6n9alWFRD+SV63WF4ZmhcT9m6wp1EGs9+1aBMgwsr3yxtw6aQyFzMJXp0Jtm5sgvKtKWJKh3 +9q60Y/xBTVnY3tSZZCUAIkGv3J9f diff --git a/tests/main/ack/task.yaml b/tests/main/ack/task.yaml new file mode 100644 index 00000000..88b344b3 --- /dev/null +++ b/tests/main/ack/task.yaml @@ -0,0 +1,31 @@ +summary: Check snap ack +systems: [-ubuntu-core-16-arm-*] +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "Ack the test store key in case" + snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" + + ALICE_ID=BGLTY1rcRKQQMbt9B407lDH38lbCW3wg + + echo "Ack when missing prerequisite fails" + ! snap ack alice.account-key + + echo "Ack account and account-key for alice" + snap ack alice.account + snap ack alice.account-key + + echo "We got alice account and account-key in the system db" + snap known account username=alice | MATCH "account-id: ${ALICE_ID}" + snap known account-key public-key-sha3-384=s2I2irs5PDzHx8n_-lEjkVUn81dvKujEmUiS0c3vwPbwojxDT_QUZ6ejDavhj_yU | MATCH "account-id: ${ALICE_ID}" + + BOB_ID=ct1P6H12NnpJ1nj2jxNX94lHp6sHClxT + + echo "Ack bob assertions as a stream" + snap ack bob.assertions + + echo "We got bob account and account-key in the system db" + snap known account username=bob | MATCH "account-id: ${BOB_ID}" + snap known account-key public-key-sha3-384=kW5sfrKZI2rIAT70JkttRq2VlNa9t8EHOoWrL2ZBAa7tLWZMy2KBweZEh3_MLcZh | MATCH "account-id: ${BOB_ID}" diff --git a/tests/main/alias/task.yaml b/tests/main/alias/task.yaml new file mode 100644 index 00000000..e1492aeb --- /dev/null +++ b/tests/main/alias/task.yaml @@ -0,0 +1,55 @@ +summary: Check snap alias and snap unalias + +prepare: | + . "$TESTSLIB/snaps.sh" + install_local aliases + +execute: | + . "$TESTSLIB/dirs.sh" + + echo "Sanity check" + aliases.cmd1|MATCH "ok command 1" + aliases.cmd2|MATCH "ok command 2" + + echo "Create manual aliases" + snap alias aliases.cmd1 alias1|MATCH ".*- aliases.cmd1 as alias1.*" + snap alias aliases.cmd2 alias2 + + echo "Test the aliases" + test -h "$SNAP_MOUNT_DIR/bin/alias1" + test -h "$SNAP_MOUNT_DIR/bin/alias2" + alias1|MATCH "ok command 1" + alias2|MATCH "ok command 2" + + echo "Check listing" + snap aliases|MATCH "aliases.cmd1 +alias1 +manual" + snap aliases|MATCH "aliases.cmd2 +alias2 +manual" + + echo "Disable one manual alias" + snap unalias alias2|MATCH ".*- aliases.cmd2 as alias2.*" + + echo "One still works, one is not there" + alias1|MATCH "ok command 1" + test ! -e "$SNAP_MOUNT_DIR/bin/alias2" + alias2 2>&1|MATCH "alias2: command not found" + + echo "Check listing again" + snap aliases|MATCH "aliases.cmd1 +alias1 +manual" + ! snap aliases|MATCH "aliases.cmd2 +alias2" + + echo "Disable all aliases" + snap unalias aliases|MATCH ".*- aliases.cmd1 as alias1*" + + echo "Alias is gone" + test ! -e "$SNAP_MOUNT_DIR/bin/alias1" + alias1 2>&1|MATCH "alias1: command not found" + ! snap aliases|MATCH "aliases.cmd1 +alias1" + + echo "Recreate one" + snap alias aliases.cmd1 alias1 + alias1|MATCH "ok command 1" + + echo "Removing the snap should remove the aliases" + snap remove aliases + test ! -e "$SNAP_MOUNT_DIR/bin/alias1" + test ! -e "$SNAP_MOUNT_DIR/bin/alias2" diff --git a/tests/main/auth-errors/task.yaml b/tests/main/auth-errors/task.yaml new file mode 100644 index 00000000..41e27963 --- /dev/null +++ b/tests/main/auth-errors/task.yaml @@ -0,0 +1,26 @@ +summary: Check that the authentication errors are properly reported. + +systems: [-ubuntu-core-16-*] + +prepare: | + mkdir -p /home/test/.snap + echo -n "{\"macaroon\":\"yummy\",\"discharges\":[ \"some \"]}" > /home/test/.snap/auth.json + +restore: | + rm -rf /home/test/.snap install.output connect.output + +execute: | + echo "An unauthenticated user cannot install snaps" + if su - -c "snap install test-snapd-tools 2>${PWD}/install.output" test; then + echo "Expected error installing snap from unauthenticated account" + exit 1 + fi + expected="error: access denied (try with sudo)" + [ "$(cat install.output)" = "$expected" ] + + echo "An unauthenticated user cannot connect plugs to slots" + if su - -c "snap connect foo:bar baz:fromp 2>${PWD}/connect.output" test; then + echo "Expected error connecting plugs to slots from unauthenticated account" + exit 1 + fi + [ "$(cat connect.output)" = "$expected" ] diff --git a/tests/main/auto-aliases/task.yaml b/tests/main/auto-aliases/task.yaml new file mode 100644 index 00000000..dd23f5a5 --- /dev/null +++ b/tests/main/auto-aliases/task.yaml @@ -0,0 +1,37 @@ +summary: Check auto-aliases mechanism +execute: | + . "$TESTSLIB/dirs.sh" + + echo "Install the snap with auto-aliases" + snap install test-snapd-auto-aliases + + echo "Test the auto-aliases" + test -h "$SNAP_MOUNT_DIR/bin/test_snapd_wellknown1" + test -h "$SNAP_MOUNT_DIR/bin/test_snapd_wellknown2" + test_snapd_wellknown1|MATCH "ok wellknown 1" + test_snapd_wellknown2|MATCH "ok wellknown 2" + + echo "Check listing" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown1 +test_snapd_wellknown1 +-" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2 +-" + + echo "Removing the snap should remove the aliases" + snap remove test-snapd-auto-aliases + test ! -e "$SNAP_MOUNT_DIR/bin/test_snapd_wellknown1" + test ! -e "$SNAP_MOUNT_DIR/bin/test_snapd_wellknown2" + ! snap aliases|MATCH "test-snapd-auto-aliases.wellknown1 +test_snapd_wellknown1" + ! snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2" + + echo "Installing the snap with --unaliased doesn't create the aliases" + snap install --unaliased test-snapd-auto-aliases + test ! -e "$SNAP_MOUNT_DIR/bin/test_snapd_wellknown1" + test ! -e "$SNAP_MOUNT_DIR/bin/test_snapd_wellknown2" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown1 +test_snapd_wellknown1 +disabled" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2 +disabled" + + echo "snap prefer will enable them after the fact" + snap prefer test-snapd-auto-aliases + test -h "$SNAP_MOUNT_DIR/bin/test_snapd_wellknown1" + test -h "$SNAP_MOUNT_DIR/bin/test_snapd_wellknown2" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown1 +test_snapd_wellknown1 +-" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2 +-" diff --git a/tests/main/auto-refresh/task.yaml b/tests/main/auto-refresh/task.yaml new file mode 100644 index 00000000..e7066304 --- /dev/null +++ b/tests/main/auto-refresh/task.yaml @@ -0,0 +1,49 @@ +summary: Check that auto-refresh works + +prepare: | + snap install --devmode jq + +restore: | + . "$TESTSLIB/dirs.sh" + +execute: | + . "$TESTSLIB/dirs.sh" + + echo "Auto refresh information is shown" + output=$(snap refresh --time) + for expected in ^schedule: ^last: ^next:; do + echo "$output" | MATCH "$expected" + done + + echo "Install a snap from stable" + snap install test-snapd-tools + snap list | MATCH 'test-snapd-tools +[0-9]+\.[0-9]+' + if [ -e "$SNAP_MOUNT_DIR/core/current/meta/hooks/configure" ]; then + snap set core refresh.schedule="0:00-23:59" + snap set core refresh.disabled=false + fi + + systemctl stop snapd.{service,socket} + echo "Modify the snap to track the edge channel" + jq '.data.snaps["test-snapd-tools"].channel = "edge"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json + jq ".data[\"last-refresh\"] = \"2007-08-22T09:30:44.449455783+01:00\"" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new + mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json + + systemctl start snapd.{service,socket} + + echo "wait for auto-refresh to happen" + for _ in $(seq 120); do + if snap changes|grep -q "Done.*Auto-refresh snap \"test-snapd-tools\""; then + break + fi + echo "Ensure refresh" + snap debug ensure-state-soon + sleep 5 + done + + echo "Ensure our snap got updated" + snap list|MATCH "test-snapd-tools +[0-9]+\.[0-9]+\+fake1" + + echo "Ensure refresh.last is set" + jq ".data[\"last-refresh\"]" /var/lib/snapd/state.json | MATCH "$(date +%Y)" diff --git a/tests/main/base-snaps/task.yaml b/tests/main/base-snaps/task.yaml new file mode 100644 index 00000000..357b42fb --- /dev/null +++ b/tests/main/base-snaps/task.yaml @@ -0,0 +1,41 @@ +summary: Check that base snaps work +systems: [-opensuse-*] + +execute: | + . $TESTSLIB/snaps.sh + + echo "Ensure a snap that requires a unavailable base snap can not be installed" + snap pack $TESTSLIB/snaps/test-snapd-requires-base + if install_local test-snapd-requires-base; then + echo "ERROR: test-snapd-requires-base should not be installable without test-snapd-base" + exit 1 + fi + + echo "Ensure a base snap can be installed" + snap pack $TESTSLIB/snaps/test-snapd-base + install_local test-snapd-base + snap list | MATCH test-snapd-base + + echo "With test-snapd-base installed we now can install test-snapd-requires-base" + install_local test-snapd-requires-base + snap list | MATCH test-snapd-requires-base + + echo "Ensure the bare base works" + + if [ "$(uname -m)" != "x86_64" ]; then + echo "This test can only run on amd64 right now because snapcraft " + echo "cannot current generate binaries without wrapper scripts." + echo "Check: https://github.com/snapcore/snapcraft/pull/1420" + echo "and: https://code.launchpad.net/~snappy-dev/snappy-hub/test-snapd-busybox-static" + exit 0 + fi + + snap install --edge --devmode test-snapd-busybox-static + echo "Ensure we can run a statically linked binary from an empty base" + test-snapd-busybox-static.busybox-static echo hello | MATCH hello + + if test-snapd-busybox-static.busybox-static ls /bin/dd; then + echo "bare should be empty but it is not:" + test-snapd-busybox-static.busybox-static ls /bin + exit 1 + fi \ No newline at end of file diff --git a/tests/main/canonical-livepatch/task.yaml b/tests/main/canonical-livepatch/task.yaml new file mode 100644 index 00000000..6de7c3e5 --- /dev/null +++ b/tests/main/canonical-livepatch/task.yaml @@ -0,0 +1,19 @@ +summary: Ensure canonical-livepatch snap works + +# livepatch works only on 16.04 amd64 systems +systems: [ubuntu-16.04-64] + +execute: | + echo "Ensure canonical-livepatch can be installed" + snap install canonical-livepatch + + echo "Wait for it to respond" + for i in $(seq 30); do + if canonical-livepatch status > /dev/null 2>&1 ; then + break + fi + sleep .5 + done + + echo "And ensure we get the expected status" + canonical-livepatch status | MATCH "Machine is not enabled" diff --git a/tests/main/catalog-update/task.yaml b/tests/main/catalog-update/task.yaml new file mode 100644 index 00000000..2ac6a50a --- /dev/null +++ b/tests/main/catalog-update/task.yaml @@ -0,0 +1,16 @@ +summary: Ensure catalog update works + +execute: | + echo "Ensure that catalog refresh happens on startup" + for i in seq 60; do + if journalctl -u snapd | MATCH "Catalog refresh"; then + break + fi + done + journalctl -u snapd | MATCH "Catalog refresh" + + echo "Ensure that we don't log all catalog body data" + if journalctl -u snapd | MATCH "Tools for testing the snapd application"; then + echo "Catalog update is doing verbose http logging (it should not)." + exit 1 + fi diff --git a/tests/main/cgroup-freezer/task.yaml b/tests/main/cgroup-freezer/task.yaml new file mode 100644 index 00000000..c5e97172 --- /dev/null +++ b/tests/main/cgroup-freezer/task.yaml @@ -0,0 +1,42 @@ +summary: Each snap process is moved to appropriate freezer cgroup +details: | + This test creates a snap process that suspends itself and ensures that it + placed into the appropriate hierarchy under the freezer cgroup. +prepare: | + . $TESTSLIB/snaps.sh + install_local test-snapd-sh +execute: | + # Start a "sleep" process in the background + test-snapd-sh -c 'touch $SNAP_DATA/1.stamp && exec sleep 1h' & + pid1=$! + # Ensure that snap-confine has finished its task and that the snap process + # is active. Note that we don't want to wait forever either. + for i in $(seq 30); do + test -e /var/snap/test-snapd-sh/current/1.stamp && break + sleep 0.1 + done + # While the process is alive its PID can be seen in the tasks file of the + # control group. + cat /sys/fs/cgroup/freezer/snap.test-snapd-sh/tasks | MATCH "$pid1" + + # Start a second process so that we can check adding tasks to an existing + # control group. + test-snapd-sh -c 'touch $SNAP_DATA/2.stamp && exec sleep 1h' & + pid2=$! + for i in $(seq 30); do + test -e /var/snap/test-snapd-sh/current/2.stamp && break + sleep 0.1 + done + cat /sys/fs/cgroup/freezer/snap.test-snapd-sh/tasks | MATCH "$pid2" + + # When the process terminates the control group is updated and the task no + # longer registers there. + kill "$pid1" + wait -n || true # wait returns the exit code and we kill the process + cat /sys/fs/cgroup/freezer/snap.test-snapd-sh/tasks | MATCH -v "$pid1" + + kill "$pid2" + wait -n || true # same as above + cat /sys/fs/cgroup/freezer/snap.test-snapd-sh/tasks | MATCH -v "$pid2" +restore: | + rmdir /sys/fs/cgroup/freezer/snap.test-snapd-sh || true diff --git a/tests/main/change-errors/task.yaml b/tests/main/change-errors/task.yaml new file mode 100644 index 00000000..e6b86c05 --- /dev/null +++ b/tests/main/change-errors/task.yaml @@ -0,0 +1,10 @@ +summary: Checks for cli errors of the tasks / change command. + +execute: | + echo "When an invalid ID is given to the tasks command it shows an error" + if snap tasks 10000000; then + echo "Expected error when trying change on invalid ID" && exit 1 + fi + if snap change 10000000; then + echo "Expected error when trying change on invalid ID" && exit 1 + fi diff --git a/tests/main/chattr/task.yaml b/tests/main/chattr/task.yaml new file mode 100644 index 00000000..698b288d --- /dev/null +++ b/tests/main/chattr/task.yaml @@ -0,0 +1,19 @@ +summary: test chattr +# ubuntu-core doesn't have go :-) +# ppc64el disabled because of https://github.com/snapcore/snapd/issues/2503 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] +prepare: | + go build -o toggle ./toggle.go +execute: | + touch foo + # no immutable flag: + lsattr foo | MATCH -v i + test "$(./toggle foo)" = "mutable -> immutable" + # and now an immutable flag!: + lsattr foo | MATCH i + test "$(./toggle foo)" = "immutable -> mutable" + # no immutable flag again: + lsattr foo | MATCH -v i +restore: | + rm -f foo + rm -f toggle diff --git a/tests/main/chattr/toggle.go b/tests/main/chattr/toggle.go new file mode 100644 index 00000000..79f36ac6 --- /dev/null +++ b/tests/main/chattr/toggle.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "os" + + "github.com/snapcore/snapd/osutil" +) + +func die(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func main() { + if len(os.Args) < 2 { + die(fmt.Errorf("usage: %s file", os.Args[0])) + } + + f, err := os.Open(os.Args[1]) + if err != nil { + die(err) + } + + before, err := osutil.GetAttr(f) + if err != nil { + die(err) + } + + err = osutil.SetAttr(f, before^osutil.FS_IMMUTABLE_FL) + if err != nil { + die(err) + } + + after, err := osutil.GetAttr(f) + if err != nil { + die(err) + } + + if before&osutil.FS_IMMUTABLE_FL != 0 { + fmt.Print("immutable") + } else { + fmt.Print("mutable") + } + fmt.Print(" -> ") + if after&osutil.FS_IMMUTABLE_FL != 0 { + fmt.Println("immutable") + } else { + fmt.Println("mutable") + } +} diff --git a/tests/main/classic-confinement-not-supported/task.yaml b/tests/main/classic-confinement-not-supported/task.yaml new file mode 100644 index 00000000..d142fcce --- /dev/null +++ b/tests/main/classic-confinement-not-supported/task.yaml @@ -0,0 +1,38 @@ +summary: Ensure that classic confinement works + +environment: + CLASSIC_SNAP: test-snapd-classic-confinement + +systems: [ubuntu-core-*, fedora-*] + +prepare: | + . $TESTSLIB/snaps.sh + snap pack "$TESTSLIB/snaps/$CLASSIC_SNAP/" + +execute: | + . $TESTSLIB/strings.sh + + echo "Check that classic snaps work only with --classic" + if snap install --dangerous "${CLASSIC_SNAP}_1.0_all.snap"; then + echo "snap install needs --classic to install local snaps with classic confinment" + exit 1 + fi + + if snap install $CLASSIC_SNAP; then + echo "snap install needs --classic to install remote snaps with classic confinment" + exit 1 + fi + + echo "Check that the classic snap is not installable even with --classic" + EXPECTED_TEXT="cannot install snap file: classic confinement is only supported on classic systems" + if [[ "$SPREAD_SYSTEM" = fedora-* ]]; then + EXPECTED_TEXT="classic confinement requires snaps under /snap or symlink from /snap to /var/lib/snapd/snap" + fi + str_to_one_line "$( snap install --dangerous --classic "${CLASSIC_SNAP}_1.0_all.snap" 2>&1 && exit 1 || true )" | MATCH "$EXPECTED_TEXT" + + echo "Not from the store either" + EXPECTED_TEXT="snap \"$CLASSIC_SNAP\" requires classic confinement which is only available on classic systems" + if [[ "$SPREAD_SYSTEM" = fedora-* ]]; then + EXPECTED_TEXT="cannot install \"$CLASSIC_SNAP\": classic confinement requires snaps under /snap or symlink from /snap to /var/lib/snapd/snap" + fi + str_to_one_line "$( snap install --classic "$CLASSIC_SNAP" 2>&1 && exit 1 || true )" | MATCH "$EXPECTED_TEXT" diff --git a/tests/main/classic-confinement/task.yaml b/tests/main/classic-confinement/task.yaml new file mode 100644 index 00000000..dd16b20f --- /dev/null +++ b/tests/main/classic-confinement/task.yaml @@ -0,0 +1,42 @@ +summary: Ensure that classic confinement works + +environment: + CLASSIC_SNAP: test-snapd-classic-confinement + +# Classic confinement isn't working yet on Fedora +systems: [-ubuntu-core-*, -fedora-*] + +prepare: | + . $TESTSLIB/snaps.sh + snap pack "$TESTSLIB/snaps/$CLASSIC_SNAP/" + +execute: | + echo "Check that classic snaps work only with --classic" + if snap install --dangerous "${CLASSIC_SNAP}_1.0_all.snap"; then + echo "snap install needs --classic to install local snaps with classic confinment" + exit 1 + fi + + if snap install "$CLASSIC_SNAP"; then + echo "snap install needs --classic to install remote snaps with classic confinment" + exit 1 + fi + + echo "Check that the classic snap works (it skips the entire sandbox)" + snap install --dangerous --classic "${CLASSIC_SNAP}_1.0_all.snap" + touch /tmp/lala + "$CLASSIC_SNAP" | MATCH lala + snap remove "$CLASSIC_SNAP" + + echo "Check that we can install classic confinement snaps from the store" + snap install --classic "$CLASSIC_SNAP" + snap list | MATCH "$CLASSIC_SNAP .*1.0 .*classic" + snap info "$CLASSIC_SNAP"|MATCH "installed:.* 1.0 .*classic" + "$CLASSIC_SNAP" | MATCH lala + + echo "Snap refresh from the store also works (2.0 is in beta, 1.0 in stable)" + snap refresh --beta "$CLASSIC_SNAP" + snap list | MATCH "$CLASSIC_SNAP .*2.0 .*classic" + snap info "$CLASSIC_SNAP"|MATCH "installed:.* 2.0 .*classic" + "$CLASSIC_SNAP" | MATCH lala + diff --git a/tests/main/classic-custom-device-reg/task.yaml b/tests/main/classic-custom-device-reg/task.yaml new file mode 100644 index 00000000..72bfea54 --- /dev/null +++ b/tests/main/classic-custom-device-reg/task.yaml @@ -0,0 +1,75 @@ +summary: | + Test gadget customized device initialisation and registration also on classic +systems: [-ubuntu-core-16-*] +environment: + SEED_DIR: /var/lib/snapd/seed +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . "$TESTSLIB/systemd.sh" + + snap pack "$TESTSLIB/snaps/classic-gadget" + snap download "--$CORE_CHANNEL" core + + "$TESTSLIB/reset.sh" --keep-stopped + mkdir -p "$SEED_DIR/snaps" + mkdir -p "$SEED_DIR/assertions" + cat > "$SEED_DIR/seed.yaml" < "$SEED_DIR/seed.yaml" </dev/null |grep -q -E "^basic" ; then + break + fi + sleep 1 + done + + echo "Verifying the imported assertions" + if ! snap known model | MATCH "model: my-classic" ; then + echo "Model assertion was not imported on firstboot" + exit 1 + fi + + snap list | MATCH "^basic" + test -f "$SEED_DIR/snaps/basic.snap" diff --git a/tests/main/classic-ubuntu-core-transition-auth/task.yaml b/tests/main/classic-ubuntu-core-transition-auth/task.yaml new file mode 100644 index 00000000..ffeef914 --- /dev/null +++ b/tests/main/classic-ubuntu-core-transition-auth/task.yaml @@ -0,0 +1,67 @@ +summary: Ensure that the ubuntu-core -> core transition works with auth.json + +# we never test on core because the transition can only happen on "classic" +# we disable on ppc64el because the downloads are very slow there +# Both Fedora and openSUSE are disabled at the moment as there is something +# fishy going on and the snapd service gets terminated during the process. +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el, -fedora-*, -opensuse-*] + +warn-timeout: 1m +kill-timeout: 5m +debug: | + snap changes + . "$TESTSLIB/changes.sh" + snap change "$(change_id 'Transition ubuntu-core to core')" || true +execute: | + . "$TESTSLIB/pkgdb.sh" + echo "Ensure core is gone and we have ubuntu-core instead" + distro_purge_package snapd + distro_install_build_snapd + + snap download --${CORE_CHANNEL} ubuntu-core + snap ack ./ubuntu-core_*.assert + snap install ./ubuntu-core_*.snap + + mkdir -p /root/.snap/ + echo '{}' > /root/.snap/auth.json + mkdir -p /home/test/.snap/ + echo '{}' > /home/test/.snap/auth.json + + echo "Ensure transition is triggered" + # wait for steady state or ensure-state-soon will be pointless + ok=0 + for i in $(seq 40); do + if ! snap changes|grep -q ".*.Doing.*" ; then + ok=1 + break + fi + sleep .5 + done + if [ $ok -ne 1 ] ; then + echo "Did not reach steady state" + exit 1 + fi + snap debug ensure-state-soon + + + # wait for transition + ok=0 + for i in $(seq 240); do + if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then + ok=1 + break + fi + sleep 1 + done + if [ $ok -ne 1 ] ; then + echo "Transition did not start or finish" + exit 1 + fi + + if snap list|grep ubuntu-core; then + echo "ubuntu-core still installed, transition failed" + exit 1 + fi + + echo "Ensure interfaces are connected" + snap interfaces | MATCH ":core-support.*core:core-support-plug" diff --git a/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml b/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml new file mode 100644 index 00000000..93a46d37 --- /dev/null +++ b/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml @@ -0,0 +1,72 @@ +summary: Ensure that the ubuntu-core -> core transition works with two cores + +# we never test on core because the transition can only happen on "classic" +# we disable on ppc64el because the downloads are very slow there +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] + +warn-timeout: 1m +kill-timeout: 5m +debug: | + snap changes + . $TESTSLIB/changes.sh + snap change "$(change_id 'Transition ubuntu-core to core')" || true +execute: | + echo "install a snap" + snap install test-snapd-python-webserver + snap interfaces |MATCH ":network.*test-snapd-python-webserver" + + . "$TESTSLIB/names.sh" + cp /var/lib/snapd/state.json /var/lib/snapd/state.json.old + cat /var/lib/snapd/state.json.old |jq -r '.data.snaps["core"].type="xxx"' > /var/lib/snapd/state.json + + systemctl stop snapd.service snapd.socket + systemctl start snapd.service snapd.socket + + snap download --${CORE_CHANNEL} ubuntu-core + snap ack ./ubuntu-core_*.assert + snap install ./ubuntu-core_*.snap + + cp /var/lib/snapd/state.json /var/lib/snapd/state.json.old + cat /var/lib/snapd/state.json.old |jq -r '.data.snaps["core"].type="os"' > /var/lib/snapd/state.json + + snap list | MATCH "ubuntu-core " + snap list | MATCH "core " + + echo "Ensure transition is triggered" + # wait for steady state or ensure-state-soon will be pointless + ok=0 + for i in $(seq 40); do + if ! snap changes|grep -q ".*.Doing.*" ; then + ok=1 + break + fi + sleep .5 + done + if [ $ok -ne 1 ] ; then + echo "Did not reach steady state" + exit 1 + fi + snap debug ensure-state-soon + + # wait for transition + ok=0 + for i in $(seq 240); do + if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then + ok=1 + break + fi + sleep 1 + done + if [ $ok -ne 1 ] ; then + echo "Transition did not start or finish" + exit 1 + fi + + if ! snap list|MATCH -v ubuntu-core; then + echo "ubuntu-core still installed, transition failed" + exit 1 + fi + snap interfaces |MATCH ":network.*test-snapd-python-webserver" + + echo "Ensure interfaces are connected" + snap interfaces | MATCH ":core-support.*core:core-support-plug" diff --git a/tests/main/classic-ubuntu-core-transition/task.yaml b/tests/main/classic-ubuntu-core-transition/task.yaml new file mode 100644 index 00000000..7a6204a0 --- /dev/null +++ b/tests/main/classic-ubuntu-core-transition/task.yaml @@ -0,0 +1,122 @@ +summary: Ensure that the ubuntu-core -> core transition works + +# we never test on core because the transition can only happen on "classic" +# we disable on ppc64el because the downloads are very slow there +# Both Fedora and openSUSE are disabled at the moment as there is something +# fishy going on and the snapd service gets terminated during the process. +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el, -fedora-*, -opensuse-*, -ubuntu-*-i386] + +warn-timeout: 1m +kill-timeout: 5m + +restore: | + rm -f state.json.new + +debug: | + snap list || true + snap info core || true + snap info ubuntu-core || true + snap changes + . "$TESTSLIB/changes.sh" + snap change "$(change_id 'Transition ubuntu-core to core')" || true +execute: | + . "$TESTSLIB/pkgdb.sh" + . $TESTSLIB/systemd.sh + curl() { + local url="$1" + # sadly systemd active means not that its really ready so we wait + # here for the socket to be available + while ! netstat -t -l -n|grep :80; do + netstat -l -l -n + sleep 1 + done + python3 -c "import urllib.request; print(urllib.request.urlopen(\"$url\").read().decode(\"utf-8\"))" + } + + . "$TESTSLIB/pkgdb.sh" + echo "Ensure core is gone and we have ubuntu-core instead" + distro_purge_package snapd + distro_install_build_snapd + + # modify daemon state to set ubuntu-core-transition-last-retry-time to the + # current time to prevent the ubuntu-core transition before the test snap is + # installed + systemctl stop snapd.{service,socket} + now="$(date --utc -Ins)" + jq -c '. + {data: (.data + {"ubuntu-core-transition-last-retry-time": "'"$now"'"})}' < /var/lib/snapd/state.json > state.json.new + mv state.json.new /var/lib/snapd/state.json + systemctl start snapd.{service,socket} + + snap download "--${CORE_CHANNEL}" ubuntu-core + snap ack ./ubuntu-core_*.assert + snap install ./ubuntu-core_*.snap + + snap install test-snapd-python-webserver + snap interfaces | MATCH ":network +test-snapd-python-webserver" + snap interfaces | MATCH ":network-bind +.*test-snapd-python-webserver" + + echo "Ensure the webserver is working" + wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver + curl http://localhost | MATCH "XKCD rocks" + + # restore ubuntu-core-transition-last-retry-time to its previous value and restart the daemon + systemctl stop snapd.{service,socket} + jq -c 'del(.["data"]["ubuntu-core-transition-last-retry-time"])' < /var/lib/snapd/state.json > state.json.new + mv state.json.new /var/lib/snapd/state.json + systemctl start snapd.{service,socket} + + echo "Ensure transition is triggered" + # wait for steady state or ensure-state-soon will be pointless + ok=0 + for i in $(seq 40); do + if ! snap changes|grep -q ".*.Doing.*" ; then + ok=1 + break + fi + sleep .5 + done + if [ $ok -ne 1 ] ; then + echo "Did not reach steady state" + exit 1 + fi + snap debug ensure-state-soon + + # wait for transition + ok=0 + for i in $(seq 240); do + if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then + ok=1 + break + fi + sleep 1 + done + if [ $ok -ne 1 ] ; then + echo "Transition did not start or finish" + exit 1 + fi + + if snap list|grep ubuntu-core; then + echo "ubuntu-core still installed, transition failed" + exit 1 + fi + snap interfaces | MATCH ":network +test-snapd-python-webserver" + snap interfaces | MATCH ":network-bind +.*test-snapd-python-webserver" + echo "Ensure the webserver is still working" + wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver + curl http://localhost | MATCH "XKCD rocks" + + systemctl restart snap.test-snapd-python-webserver.test-snapd-python-webserver + wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver + echo "Ensure the webserver is working after a snap restart" + curl http://localhost | MATCH "XKCD rocks" + + echo "Ensure interfaces are connected" + snap interfaces | MATCH ":core-support.*core:core-support-plug" + + echo "Ensure snap set core works" + snap set core system.power-key-action=ignore + if [ "$(snap get core system.power-key-action)" != "ignore" ]; then + echo "snap get did not return the expected result: " + snap get core system.power-key-action + exit 1 + fi diff --git a/tests/main/cmdline/task.yaml b/tests/main/cmdline/task.yaml new file mode 100644 index 00000000..35e17234 --- /dev/null +++ b/tests/main/cmdline/task.yaml @@ -0,0 +1,10 @@ +summary: Check that cmdline for channel shortcuts work +execute: | + echo Conflicting channel commandline errors correctly + if snap install --beta --edge test-snapd-tools 2>err.msg; then + echo "Expected failure when --beta --edge is given at the same time" + exit 1 + fi + MATCH "Please specify a single channel" < err.msg +restore: | + rm -f err.msg diff --git a/tests/main/completion/abort.exp b/tests/main/completion/abort.exp new file mode 100644 index 00000000..60bc2980 --- /dev/null +++ b/tests/main/completion/abort.exp @@ -0,0 +1,7 @@ +source lib.exp0 + +# abort completes with change ids +rechat "snap abort \t\t" "1 *2" + +cancel +brexit diff --git a/tests/main/completion/ack.exp b/tests/main/completion/ack.exp new file mode 100644 index 00000000..92ffa96a --- /dev/null +++ b/tests/main/completion/ack.exp @@ -0,0 +1,8 @@ +source lib.exp0 + +# ack completes with directories and files +chat "snap ack ./\t\t" "ack.exp" true +chat "" " testdir/" + +cancel +brexit diff --git a/tests/main/completion/alias.exp b/tests/main/completion/alias.exp new file mode 100644 index 00000000..0aa871f3 --- /dev/null +++ b/tests/main/completion/alias.exp @@ -0,0 +1,13 @@ +source lib.exp0 + +# alias completes binaries, not just any istalled snap +# (here, "core" isn't offered for completion) +chat "snap alias \t" "test-snapd-tools." +chat "env myenv\n" "test-snapd-tools.env as myenv" + +# unalias completes aliases and snaps that have aliases +chat "snap unalias \t\t" "myenv *test-snapd-tools" +chat "m\t\n" "test-snapd-tools.env as myenv" + +cancel +brexit diff --git a/tests/main/completion/buy.exp b/tests/main/completion/buy.exp new file mode 100644 index 00000000..ef3f4d4e --- /dev/null +++ b/tests/main/completion/buy.exp @@ -0,0 +1,8 @@ +source lib.exp0 + +# buy completes remote snaps only +# chat "snap buy \t\t" "snap buy ??$" +chat "snap buy test-snapd-t\t\t" "test-snapd-thumbnailer*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/change.exp b/tests/main/completion/change.exp new file mode 100644 index 00000000..34eb975c --- /dev/null +++ b/tests/main/completion/change.exp @@ -0,0 +1,7 @@ +source lib.exp0 + +# change completes with change ids +rechat "snap change \t\t" "1 *2" + +cancel +brexit diff --git a/tests/main/completion/delete-key.exp b/tests/main/completion/delete-key.exp new file mode 100644 index 00000000..3ed73135 --- /dev/null +++ b/tests/main/completion/delete-key.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap delete-key \t" "snap delete-key default" + +cancel +brexit diff --git a/tests/main/completion/disable.exp b/tests/main/completion/disable.exp new file mode 100644 index 00000000..1187b3b8 --- /dev/null +++ b/tests/main/completion/disable.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap disable \t\t" "core*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/download.exp b/tests/main/completion/download.exp new file mode 100644 index 00000000..4a773d14 --- /dev/null +++ b/tests/main/completion/download.exp @@ -0,0 +1,7 @@ +source lib.exp0 + +# download with 3+ chars completes store stuff +chat "snap download test-\t\t" "test-assumes" + +cancel +brexit diff --git a/tests/main/completion/enable.exp b/tests/main/completion/enable.exp new file mode 100644 index 00000000..9658f10f --- /dev/null +++ b/tests/main/completion/enable.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap enable \t\t" "core*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/export-key.exp b/tests/main/completion/export-key.exp new file mode 100644 index 00000000..76c98f41 --- /dev/null +++ b/tests/main/completion/export-key.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap export-key \t" "snap export-key default" + +cancel +brexit diff --git a/tests/main/completion/get.exp b/tests/main/completion/get.exp new file mode 100644 index 00000000..0e0627f2 --- /dev/null +++ b/tests/main/completion/get.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap get \t\t" "core*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/info.exp b/tests/main/completion/info.exp new file mode 100644 index 00000000..55bc63c4 --- /dev/null +++ b/tests/main/completion/info.exp @@ -0,0 +1,11 @@ +source lib.exp0 + +# info completes directories and snap files +chat "snap inf\t./\t\t" "bar.snap*testdir/" +cancel + +# also remote snaps +chat "snap info test-\t\t" "test-assumes" + +cancel +brexit diff --git a/tests/main/completion/install.exp b/tests/main/completion/install.exp new file mode 100644 index 00000000..2a443137 --- /dev/null +++ b/tests/main/completion/install.exp @@ -0,0 +1,22 @@ +source lib.exp0 + +# install completes directories and snaps +chat "snap ins\t./\t\t" "bar.snap*testdir/" +cancel + +# install also completes store stuff +chat "snap install tes\t\t\t" "test-assumes" true +# TODO: don't list things already installed: +chat "" "test-snapd-tools" true +chat "" "testdir/" +cancel + +# no extra space added after a directory +chat "snap ins\t./tes\t" " ./testdir/$" +cancel + +# extra space added after a file +chat "snap ins\t./bar\t" " ./bar.snap $" + +cancel +brexit diff --git a/tests/main/completion/key.exp0 b/tests/main/completion/key.exp0 new file mode 100644 index 00000000..c138b15c --- /dev/null +++ b/tests/main/completion/key.exp0 @@ -0,0 +1,17 @@ +source lib.exp0 + +# create a key doesn't complete +chat "snap create-key \t\t" "snap create-key ??$" +chat "\r" "Passphrase:" +sleep .5 +chat "pass\r" "Confirm passphrase:" +sleep .5 +send "pass\r" + +# this can take a while +set timeout 60 + +next +cancel +brexit + diff --git a/tests/main/completion/lib.exp0 b/tests/main/completion/lib.exp0 new file mode 120000 index 00000000..93dd3fbe --- /dev/null +++ b/tests/main/completion/lib.exp0 @@ -0,0 +1 @@ +../../completion/lib.exp0 \ No newline at end of file diff --git a/tests/main/completion/list.exp b/tests/main/completion/list.exp new file mode 100644 index 00000000..614281f3 --- /dev/null +++ b/tests/main/completion/list.exp @@ -0,0 +1,7 @@ +source lib.exp0 + +# list completes locally installed snaps +chat "snap list \t\t" "core*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/refresh.exp b/tests/main/completion/refresh.exp new file mode 100644 index 00000000..68024efb --- /dev/null +++ b/tests/main/completion/refresh.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap refresh \t\t" "core*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/remove.exp b/tests/main/completion/remove.exp new file mode 100644 index 00000000..9bb197a4 --- /dev/null +++ b/tests/main/completion/remove.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap remove \t\t" "core*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/revert.exp b/tests/main/completion/revert.exp new file mode 100644 index 00000000..366d2d34 --- /dev/null +++ b/tests/main/completion/revert.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap revert \t\t" "core*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/set.exp b/tests/main/completion/set.exp new file mode 100644 index 00000000..b8c97f54 --- /dev/null +++ b/tests/main/completion/set.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap set \t\t" "core*test-snapd-tools" + +cancel +brexit diff --git a/tests/main/completion/sign-build.exp b/tests/main/completion/sign-build.exp new file mode 100644 index 00000000..2eb65175 --- /dev/null +++ b/tests/main/completion/sign-build.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap sign-build -k\t" "kdefault" + +cancel +brexit diff --git a/tests/main/completion/sign.exp b/tests/main/completion/sign.exp new file mode 100644 index 00000000..d3949e79 --- /dev/null +++ b/tests/main/completion/sign.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap sign -k\t" "kdefault" + +cancel +brexit diff --git a/tests/main/completion/task.yaml b/tests/main/completion/task.yaml new file mode 100644 index 00000000..9938cf9a --- /dev/null +++ b/tests/main/completion/task.yaml @@ -0,0 +1,45 @@ +summary: Check different completions + +systems: + - -ubuntu-core-16-* + # ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 + - -ubuntu-*-ppc64el + +environment: + NAMES: /var/cache/snapd/names + +prepare: | + systemctl stop snapd.service snapd.socket + [ -e "$NAMES" ] && mv "$NAMES" "$NAMES.orig" + cat >"$NAMES" < $@ + +.ONESHELL: + +.PHONY: clean +clean: + rm -f test-snapd-hello-classic.*.bin + rm -f meta/snap.yaml + rm -f *.snap + rm -f -d meta diff --git a/tests/main/confinement-classic/test-snapd-hello-classic/test-snapd-hello-classic.c b/tests/main/confinement-classic/test-snapd-hello-classic/test-snapd-hello-classic.c new file mode 100644 index 00000000..29853e12 --- /dev/null +++ b/tests/main/confinement-classic/test-snapd-hello-classic/test-snapd-hello-classic.c @@ -0,0 +1,12 @@ +#include +#include + +int main(int argc, char **argv) +{ + if (argc == 1) { + printf("Hello Classic!\n"); + } else { + printf("TMPDIR=%s\n", getenv("TMPDIR")); + } + return 0; +} diff --git a/tests/main/core-snap-not-test-test/task.yaml b/tests/main/core-snap-not-test-test/task.yaml new file mode 100644 index 00000000..ae23c744 --- /dev/null +++ b/tests/main/core-snap-not-test-test/task.yaml @@ -0,0 +1,5 @@ +summary: Files inside the core snap are not owned by test.test +execute: | + [ $(find /snap/core/current/ -user test | wc -l) = 0 ] +debug: | + find /snap/core/current/ -user test diff --git a/tests/main/core-snap-refresh-on-core/task.yaml b/tests/main/core-snap-refresh-on-core/task.yaml new file mode 100644 index 00000000..b8602e6d --- /dev/null +++ b/tests/main/core-snap-refresh-on-core/task.yaml @@ -0,0 +1,122 @@ +summary: Check that the core snap can be refreshed on a core device + +systems: [ubuntu-core-16-64] + +details: | + This test checks that the core snap can be refreshed from an installed + revision to a new one. It expects to find a new snap revision in the + channel pointed by the NEW_CORE_CHANNEL env var. + +manual: true + +restore: | + rm -f prevBoot nextBoot + rm -f core_*.{assert,snap} + +prepare: | + snap install test-snapd-tools + +execute: | + wait_core_pre_boot() { + chg_id="$1" + + # save change id to wait later or abort + echo ${chg_id} >curChg + + # wait for the link task to be done + while ! snap change ${chg_id}|grep -q "^Done.*Make snap.*available to the system" ; do sleep 1 ; done + + } + wait_core_post_boot() { + # booted + while [ "$(bootenv snap_mode)" != "" ]; do + sleep 1 + done + # and change fully done + while ! snap changes | grep "^$(cat curChg).* Done "; do + sleep 1; + done + } + + if [ "$NEW_CORE_CHANNEL" = "" ]; then + echo "please set the SPREAD_NEW_CORE_CHANNEL environment" + exit 1 + fi + + . $TESTSLIB/boot.sh + if [ "$SPREAD_REBOOT" = 0 ]; then + # ensure we have a good starting place + + # sanity + test-snapd-tools.echo hello | MATCH hello + + # go to known good starting place + snap download core --${CORE_CHANNEL} + snap ack core_*.assert + wait_core_pre_boot $(snap install --no-wait core_*.snap) + REBOOT + + elif [ "$SPREAD_REBOOT" = 1 ]; then + # from our good starting place we refresh + + wait_core_post_boot + + # save current core revision + snap list | awk "/^core / {print(\$3)}" > prevBoot + + # refresh + wait_core_pre_boot $(snap refresh core --${NEW_CORE_CHANNEL} --no-wait) + + # check boot env vars + snap list | awk "/^core / {print(\$3)}" > nextBoot + + test "$(bootenv snap_core)" = "core_$(cat prevBoot).snap" + test "$(bootenv snap_try_core)" = "core_$(cat nextBoot).snap" + + # there are no errors in the changes list + ! snap changes | MATCH '^[0-9]+ +Error' + + # test-snapd-tools works + test-snapd-tools.echo hello | MATCH hello + + REBOOT + elif [ "$SPREAD_REBOOT" = 2 ]; then + # after refresh to NEW_CHANNEL + + wait_core_post_boot + + # check boot env vars + test "$(bootenv snap_core)" = "core_$(cat nextBoot).snap" + test "$(bootenv snap_try_core)" = "" + + # and there are no errors in the changes list + ! snap changes | MATCH '^[0-9]+ +Error' + + # test-snapd-tools works + test-snapd-tools.echo hello | MATCH hello + + # revert core + wait_core_pre_boot $(snap revert core --no-wait) + + test "$(bootenv snap_core)" = "core_$(cat nextBoot).snap" + test "$(bootenv snap_try_core)" = "core_$(cat prevBoot).snap" + + # there are no errors in the changes list + ! snap changes | MATCH '^[0-9]+ +Error' + + REBOOT + elif [ "$SPREAD_REBOOT" = 3 ]; then + # after revert + + wait_core_post_boot + + # check that we reverted + test "$(bootenv snap_core)" = "core_$(cat prevBoot).snap" + test "$(bootenv snap_try_core)" = "" + + # and there are no errors in the changes list + ! snap changes | MATCH '^[0-9]+ +Error' + + # test-snapd-tools works + test-snapd-tools.echo hello | MATCH hello + fi diff --git a/tests/main/core-snap-refresh/task.yaml b/tests/main/core-snap-refresh/task.yaml new file mode 100644 index 00000000..6c6f3904 --- /dev/null +++ b/tests/main/core-snap-refresh/task.yaml @@ -0,0 +1,48 @@ +summary: Check that the core snap can be refreshed + +details: | + This test checks that the core snap can be refreshed from an installed + revision to a new one. It expects to find a new snap revision in the + channel pointed by the NEW_CORE_CHANNEL env var. + +manual: true + +restore: | + rm -f prevBoot nextBoot + +execute: | + . $TESTSLIB/boot.sh + if [ "$SPREAD_REBOOT" = 0 ]; then + # save current core revision + snap list | awk "/^core / {print(\$3)}" > prevBoot + + # refresh + snap refresh core --${NEW_CORE_CHANNEL} + + # check boot env vars + snap list | awk "/^core / {print(\$3)}" > nextBoot + + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + test "$(bootenv snap_core)" = "core_$(cat prevBoot).snap" + test "$(bootenv snap_try_core)" = "core_$(cat nextBoot).snap" + fi + + # there are no errors in the changes list + ! snap changes | MATCH '^[0-9]+ +Error' + + REBOOT + fi + # after upgrade + # the boot env vars are correctly set + echo "Waiting for snapd to clean snap_mode" + while [ "$(bootenv snap_mode)" != "" ]; do + sleep 1 + done + + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + test "$(bootenv snap_core)" = "core_$(cat nextBoot).snap" + test "$(bootenv snap_try_core)" = "" + fi + + # and there are no errors in the changes list + ! snap changes | MATCH '^[0-9]+ +Error' diff --git a/tests/main/create-key/passphrase_mismatch.exp b/tests/main/create-key/passphrase_mismatch.exp new file mode 100644 index 00000000..9e85a479 --- /dev/null +++ b/tests/main/create-key/passphrase_mismatch.exp @@ -0,0 +1,17 @@ +spawn snap create-key + +expect "Passphrase: " +sleep .5 +send "one\n" + +expect "Confirm passphrase: " +sleep .5 +send "two\n" + +expect { + "error: passphrases do not match" { + exit 0 + } default { + exit 1 + } +} diff --git a/tests/main/create-key/successful_default.exp b/tests/main/create-key/successful_default.exp new file mode 100644 index 00000000..7b438d64 --- /dev/null +++ b/tests/main/create-key/successful_default.exp @@ -0,0 +1,48 @@ +set timeout 60 + +spawn snap create-key + +expect "Passphrase: " +sleep .5 +send "pass\n" + +expect "Confirm passphrase: " +sleep .5 +send "pass\n" + +set status [wait] +if {[lindex $status 3] != 0} { + exit 1 +} + +set timeout 60 + +spawn snap keys + +expect { + "default " {} + timeout { exit 1 } + eof { exit 1 } +} + +set status [wait] +if {[lindex $status 3] != 0} { + exit 1 +} + +spawn snap export-key --account=developer default + +# fun! +# gpg1 asks for a passphrase on the terminal no matter what +# gpg2 gets the passphrase via our fake pinentry +expect { + "Enter passphrase: " {send "pass\n"; exp_continue} + "account-id: developer" {} + timeout { exit 1 } + eof { exit 1 } +} + +set status [wait] +if {[lindex $status 3] != 0} { + exit 1 +} diff --git a/tests/main/create-key/successful_non_default.exp b/tests/main/create-key/successful_non_default.exp new file mode 100644 index 00000000..0b376c5a --- /dev/null +++ b/tests/main/create-key/successful_non_default.exp @@ -0,0 +1,48 @@ +set timeout 60 + +spawn snap create-key another + +expect "Passphrase: " +sleep .5 +send "pass\n" + +expect "Confirm passphrase: " +sleep .5 +send "pass\n" + +set status [wait] +if {[lindex $status 3] != 0} { + exit 1 +} + +set timeout 60 + +spawn snap keys + +expect { + "another " {} + timeout { exit 1 } + eof { exit 1 } +} + +set status [wait] +if {[lindex $status 3] != 0} { + exit 1 +} + +spawn snap export-key --account=developer another + +# fun! +# gpg1 asks for a passphrase on the terminal no matter what +# gpg2 gets the passphrase via our fake pinentry +expect { + "Enter passphrase: " {send "pass\n"; exp_continue} + "account-id: developer" {} + timeout { exit 1 } + eof { exit 1 } +} + +set status [wait] +if {[lindex $status 3] != 0} { + exit 1 +} diff --git a/tests/main/create-key/task.yaml b/tests/main/create-key/task.yaml new file mode 100644 index 00000000..4765c168 --- /dev/null +++ b/tests/main/create-key/task.yaml @@ -0,0 +1,19 @@ +summary: Checks for snap create-key +# ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] + +prepare: | + . "$TESTSLIB"/mkpinentry.sh + +debug: | + sysctl kernel.random.entropy_avail || true + +execute: | + echo "Checking passphrase mismatch error" + expect -d -f passphrase_mismatch.exp + + echo "Checking successful default key pair generation" + expect -d -f successful_default.exp + + echo "Checking successful non-default key pair generation" + expect -d -f successful_non_default.exp diff --git a/tests/main/create-user/task.yaml b/tests/main/create-user/task.yaml new file mode 100644 index 00000000..19bf8cc9 --- /dev/null +++ b/tests/main/create-user/task.yaml @@ -0,0 +1,32 @@ +summary: Ensure create-user functionality + +# Disabled for Fedora, openSUSE as both have not all options for add user +# available the `snap create-user` command requires. Needs code rework. +systems: [-ubuntu-core-16-*, -fedora-*, -opensuse-*] + +environment: + USER_EMAIL: mvo@ubuntu.com + USER_NAME: mvo + +restore: | + userdel -r $USER_NAME || true + rm -rf /etc/sudoers.d/create-user-$USER_NAME + +execute: | + echo "snap create-user -- ensure failure when run as non-root user without sudo" + expected="error: while creating user: access denied" + if obtained=$(su - test /bin/sh -c "snap create-user $USER_EMAIL 2>&1"); then + echo "create-user command should have failed" + fi + [[ "$obtained" =~ "$expected" ]] + + echo "snap create-user -- ensure success when run as non-root user with sudo" + expected="created user \"$USER_NAME\"" + obtained=$(su - test /bin/sh -c "sudo snap create-user --force-managed --sudoer $USER_EMAIL 2>&1") + [[ "$obtained" =~ "$expected" ]] + + echo "ensure user exists in /etc/passwd" + MATCH "^$USER_NAME:x:[0-9]+:[0-9]+:$USER_EMAIL" < /etc/passwd + + echo "ensure proper sudoers.d file" + MATCH "$USER_NAME ALL=\(ALL\) NOPASSWD:ALL" < /etc/sudoers.d/create-user-$USER_NAME diff --git a/tests/main/debs-have-built-using/task.yaml b/tests/main/debs-have-built-using/task.yaml new file mode 100644 index 00000000..46b53a97 --- /dev/null +++ b/tests/main/debs-have-built-using/task.yaml @@ -0,0 +1,12 @@ +summary: Ensure that our debs have the "built-using" header + +systems: [-ubuntu-core-*, -fedora-*, -opensuse-*] + +execute: | + out=$(dpkg -I $GOHOME/snapd_*.deb) + if [[ "$SPREAD_SYSTEM" = ubuntu-* ]]; then + # Apparmor & seccomp is only compiled in on Ubuntu for now. + echo $out | MATCH "Built-Using:.*apparmor \(=" + echo $out | MATCH "Built-Using:.*libseccomp \(=" + fi + echo $out | MATCH "Built-Using:.*libcap2 \(=" diff --git a/tests/main/debug-confinement/task.yaml b/tests/main/debug-confinement/task.yaml new file mode 100644 index 00000000..f14a8d3a --- /dev/null +++ b/tests/main/debug-confinement/task.yaml @@ -0,0 +1,12 @@ +summary: Verify confinement is correctly reported + +execute: | + expected=partial + case "$SPREAD_SYSTEM" in + ubuntu-*) + expected=strict + ;; + *) + ;; + esac + test "$(snap debug confinement)" = "$expected" diff --git a/tests/main/dirs-not-shared-with-host/task.yaml b/tests/main/dirs-not-shared-with-host/task.yaml new file mode 100644 index 00000000..75bf534b --- /dev/null +++ b/tests/main/dirs-not-shared-with-host/task.yaml @@ -0,0 +1,30 @@ +summary: Ensure that certain directories are coming from the core snap +details: | + The snap-confine program bind mounts the /etc directory from the + classic distribution into the snap execution environment. + Certain directories however, if they exist on the host's /etc + are actually, bind-mounted from the core snap for a more + consistent behaviour across various distributions. +systems: + # This test only applies to classic systems + - -ubuntu-core-16-* +prepare: | + echo "Having installed the test snap" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools +environment: + DIRECTORY/alternatives: /etc/alternatives + DIRECTORY/ssl: /etc/ssl +execute: | + . "$TESTSLIB/dirs.sh" + echo "We can check the inode number of $DIRECTORY" + host_inode="$(stat -c '%i' $DIRECTORY)" + if [ -e $SNAP_MOUNT_DIR/core/current ]; then + core_inode="$(stat -c '%i' $SNAP_MOUNT_DIR/core/current/$DIRECTORY)" + else + core_inode="$(stat -c '%i' $SNAP_MOUNT_DIR/ubuntu-core/current/$DIRECTORY)" + fi + effective_inode="$(test-snapd-tools.cmd stat -c '%i' $DIRECTORY)" + echo "The inode number as seen from a confined snap should be that of the $DIRECTORY from the core snap" + [ "$host_inode" != "$core_inode" ] + [ "$effective_inode" = "$core_inode" ] diff --git a/tests/main/econnreset/task.yaml b/tests/main/econnreset/task.yaml new file mode 100644 index 00000000..a0521639 --- /dev/null +++ b/tests/main/econnreset/task.yaml @@ -0,0 +1,42 @@ +summary: Ensure that ECONNRESET is handled +restore: | + echo "Remove the firewall rule again" + iptables -D OUTPUT -m owner --uid-owner $(id -u test) -j REJECT -p tcp --reject-with tcp-reset || true + + rm -f test-snapd-huge_* + +execute: | + echo "Downloading a large snap in the background" + su -c "/usr/bin/env SNAPD_DEBUG=1 snap download --edge test-snapd-huge 2>snap-download.log" test & + + echo "Wait until the download started and downloaded more than 1 MB" + for i in $(seq 40); do + if partial=$(ls test-snapd-huge_*.snap.partial | head -1); then + if [ $(stat -c%s "$partial") -gt $(( 1024 * 1024 )) ]; then + break + fi + fi + sleep .5 + done + + if [ ! -f "$partial" ] || [ $(stat -c%s "$partial") -eq 0 ]; then + echo "Partial file $partial did not start downloading, test broken" + kill -9 $(pidof snap) + exit 1 + fi + + echo "Block the download using iptables" + iptables -I OUTPUT -m owner --uid-owner $(id -u test) -j REJECT -p tcp --reject-with tcp-reset + + echo "Check that we retried" + for i in $(seq 20); do + if MATCH "Retrying.*\.snap, attempt 2" < snap-download.log; then + break + fi + sleep .5 + done + MATCH "Retrying.*\.snap, attempt 2" < snap-download.log + + # Note that the download will not be successful because of the nature of + # the netfilter testbed. When snap download retries the next attempt will + # end up with a "connection refused" error, something we do not retry diff --git a/tests/main/enable-disable-units-gpio/task.yaml b/tests/main/enable-disable-units-gpio/task.yaml new file mode 100644 index 00000000..9de48cf9 --- /dev/null +++ b/tests/main/enable-disable-units-gpio/task.yaml @@ -0,0 +1,87 @@ +summary: Check that systemd units are enabled/disabled and gpio works after rebooting + +details: | + This test makes sure that the systemd snippet created by the gpio interface + is executed after a reboot. + + It modifies the core snap to provide a gpio slot. Also, a mocked gpio node and the + required systemfs files (export and unexeport) are created as a bind mount. The test + expects that, after a snap declared a gpio plug is installed and connected, after + a reboot the systemd service tries to regenerate the gpio device node if it does not + find it. + +systems: [ubuntu-core-16-64] + +environment: + GPIO_MOCK_DIR: /home/test/gpio-mock + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "Given a snap declaring a plug on gpio is installed" + . $TESTSLIB/snaps.sh + install_local gpio-consumer + + echo "And a mocked gpio device is in place" + cat > /home/test/gpio-mock.sh <<-EOF + #!/bin/sh + if [ ! -d "$GPIO_MOCK_DIR" ]; then + # the service has just been created + mkdir -p $GPIO_MOCK_DIR + touch $GPIO_MOCK_DIR/gpio100 $GPIO_MOCK_DIR/export $GPIO_MOCK_DIR/unexport + else + # after reboot, remove device node to test export + rm $GPIO_MOCK_DIR/gpio100 + truncate -s 0 $GPIO_MOCK_DIR/export + fi + mount --bind $GPIO_MOCK_DIR /sys/class/gpio + EOF + chmod a+x /home/test/gpio-mock.sh + + cat > /etc/systemd/system/gpio-mock.service <<-EOF + [Unit] + Description=Set up mock for gpio test + Before=snap.core.interface.gpio-100.service + + [Service] + Type=oneshot + RemainAfterExit=true + ExecStart=/home/test/gpio-mock.sh + ExecStop= + + [Install] + WantedBy=multi-user.target + EOF + systemctl enable --now gpio-mock.service + + echo "And the gpio plug is connected" + snap connect gpio-consumer:gpio :gpio-pin + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + umount /sys/class/gpio || true + systemctl disable gpio-mock.service + rm -rf $GPIO_MOCK_DIR /etc/systemd/system/gpio-mock.service /home/test/gpio-mock.sh + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + echo "Then the snap service units concerning the gpio device must be run before and after a reboot" + expected="Unit snap.core.interface.gpio-100.service has finished starting up" + journalctl -xe --no-pager | MATCH "$expected" + + if [ "$SPREAD_REBOOT" = "1" ]; then + cat $GPIO_MOCK_DIR/export | MATCH "^100$" + fi + + if [ "$SPREAD_REBOOT" = "0" ]; then + REBOOT + fi diff --git a/tests/main/enable-disable/task.yaml b/tests/main/enable-disable/task.yaml new file mode 100644 index 00000000..1aab09c2 --- /dev/null +++ b/tests/main/enable-disable/task.yaml @@ -0,0 +1,43 @@ +summary: Check that enable/disable works + +execute: | + . $TESTSLIB/dirs.sh + echo "Install test-snapd-tools and ensure it runs" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + test-snapd-tools.echo Hello|MATCH Hello + echo "Disable test-snapd-tools and ensure it is listed as disabled" + snap disable test-snapd-tools|MATCH disabled + + echo "Ensure the test-snapd-tools command is no longer there" + if ls $SNAP_MOUNT_DIR/bin/test-snapd-tools*; then + echo "test-snapd-tools binaries are not disabled" + exit 1 + fi + + echo "Ensure the test-snapd-tools security profiles are no longer there" + if ls /var/lib/snapd/apparmor/profiles/snap.test-snapd-tools*; then + echo "test-snapd-tools securiry profiles are not disabled" + exit 1 + fi + + echo "Enable test-snapd-tools again and ensure it is no longer listed as disabled" + snap enable test-snapd-tools|MATCH -v disabled + + echo "Ensure the test-snapd-tools security profiles are present" + if !ls /var/lib/snapd/apparmor/profiles/snap.test-snapd-tools*; then + echo "test-snapd-tools securiry profiles are not present" + exit 1 + fi + + echo "Ensure test-snapd-tools runs normally after it was enabled" + test-snapd-tools.echo Hello |MATCH Hello + + echo "Ensure the important snaps can not be disabled" + . $TESTSLIB/names.sh + for sn in core $kernel_name $gadget_name; do + if snap disable $sn; then + echo "It should not be possible to disable $sn" + exit 1 + fi + done diff --git a/tests/main/failover/task.yaml b/tests/main/failover/task.yaml new file mode 100644 index 00000000..f599f486 --- /dev/null +++ b/tests/main/failover/task.yaml @@ -0,0 +1,113 @@ +summary: check that the core and kernel snaps roll back correctly after a failed upgrade + +systems: [ubuntu-core-16-*] + +details: | + This test ensures that the system can survive to a failed upgrade of a fundamental + snap, rolling back to the last good known version. + + The logic common to all the scenarios unpacks the target snap, injects the failure, + repacks and installs it. Then it checks that all is set for installed the snap with + the failure and executes a reboot. The test checks that after the reboot (in fact two + reboots, one for trying the upgrade and another for rolling back) the installed + fundamental snap is the good one and the boot environment variables are correctly set. + +environment: + INJECT_FAILURE/rclocalcrash: inject_rclocalcrash_failure + INJECT_FAILURE/emptysystemd: inject_emptysystemd_failure + # FIXME: disabled until we find what to do! + # fails with: ERROR cannot replace signed kernel snap with an unasserted one + #INJECT_FAILURE/emptyinitrd: inject_emptyinitrd_failure + TARGET_SNAP/rclocalcrash: core + TARGET_SNAP/emptysystemd: core + #TARGET_SNAP/emptyinitrd: kernel + BUILD_DIR: /home/tmp + +prepare: | + mkdir -p $BUILD_DIR + +restore: | + rm -f failing.snap failBoot currentBoot prevBoot + rm -rf $BUILD_DIR + + # FIXME: remove the unset when we reset properly snap_try_{core,kernel} on rollback + . $TESTSLIB/boot.sh + bootenv_unset snap_try_core + bootenv_unset snap_try_kernel + +debug: | + . $TESTSLIB/boot.sh + bootenv + snap list + snap changes + +execute: | + inject_rclocalcrash_failure(){ + chmod a+x $BUILD_DIR/unpack/etc/rc.local + cat < $BUILD_DIR/unpack/etc/rc.local + #!bin/sh + printf c > /proc/sysrq-trigger + EOF + } + + inject_emptysystemd_failure(){ + truncate -s 0 $BUILD_DIR/unpack/lib/systemd/systemd + } + + inject_emptyinitrd_failure(){ + truncate -s 0 $BUILD_DIR/unpack/initrd.img + } + + . $TESTSLIB/names.sh + . $TESTSLIB/boot.sh + if [ "$TARGET_SNAP" = kernel ]; then + TARGET_SNAP_NAME=$kernel_name + else + TARGET_SNAP_NAME=core + fi + + if [ "$SPREAD_REBOOT" = 0 ]; then + # first pass, save current target snap revision + snap list | awk "/^${TARGET_SNAP_NAME} / {print(\$3)}" > prevBoot + + # unpack current target snap + unsquashfs -d $BUILD_DIR/unpack /var/lib/snapd/snaps/${TARGET_SNAP_NAME}_$(cat prevBoot).snap + + # set failure condition + eval ${INJECT_FAILURE} + + # repack new target snap + snap pack $BUILD_DIR/unpack && mv ${TARGET_SNAP_NAME}_*.snap failing.snap + + # install new target snap + chg_id=$(snap install --dangerous failing.snap --no-wait) + + while ! snap change ${chg_id}|grep -q "^Done.*Make snap.*available to the system" ; do sleep 1 ; done + + # check boot env vars + snap list | awk "/^${TARGET_SNAP_NAME} / {print(\$3)}" > failBoot + test "$(bootenv snap_${TARGET_SNAP})" = "${TARGET_SNAP_NAME}_$(cat prevBoot).snap" + test "$(bootenv snap_try_${TARGET_SNAP})" = "${TARGET_SNAP_NAME}_$(cat failBoot).snap" + + REBOOT + fi + + # after rollback, we have the original target snap for a while + # wait until the kernel and core snap revisions are in place + while true ; do + current=$(snap list | awk "/^${TARGET_SNAP_NAME} / {print(\$3)}") + if [ "$current" = "$(cat prevBoot)" ] ; then + break + fi + sleep 1 + done + + # and the boot env vars are correctly set + echo "Waiting for snapd to clean snap_mode" + while [ "$(bootenv snap_mode)" != "" ]; do + sleep 1 + done + + test "$(bootenv snap_${TARGET_SNAP})" = "${TARGET_SNAP_NAME}_$(cat prevBoot).snap" + # FIXME: reenable the last check when we reset properly snap_try_{core,kernel} on rollback + # test "$(bootenv snap_try_${TARGET_SNAP})" = "" diff --git a/tests/main/fakestore-install/task.yaml b/tests/main/fakestore-install/task.yaml new file mode 100644 index 00000000..ae7c635a --- /dev/null +++ b/tests/main/fakestore-install/task.yaml @@ -0,0 +1,30 @@ +summary: Ensure that the fakestore works + +environment: + BLOB_DIR: $(pwd)/fake-store-blobdir + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/store.sh + teardown_fake_store $BLOB_DIR + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" + + . $TESTSLIB/store.sh + setup_fake_store $BLOB_DIR + + . $TESTSLIB/snaps.sh + snap_path=$(make_snap basic) + make_snap_installable $BLOB_DIR ${snap_path} + + snap install basic + snap info basic | MATCH "snap-id:[ ]+basic-id" diff --git a/tests/main/find-private/successful_login.exp b/tests/main/find-private/successful_login.exp new file mode 100644 index 00000000..8924a9a7 --- /dev/null +++ b/tests/main/find-private/successful_login.exp @@ -0,0 +1,12 @@ +spawn snap login $env(SPREAD_STORE_USER) + +expect "Password: " +send "$env(SPREAD_STORE_PASSWORD)\n" + +expect { + "Login successful" { + exit 0 + } default { + exit 1 + } +} diff --git a/tests/main/find-private/task.yaml b/tests/main/find-private/task.yaml new file mode 100644 index 00000000..51b260bc --- /dev/null +++ b/tests/main/find-private/task.yaml @@ -0,0 +1,48 @@ +summary: Check that find works with private snaps. + +# ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 +systems: [-ubuntu-*-ppc64el] + +details: | + These tests rely on the existence of a snap in the remote store set to private. + + In order to do the full checks, it also needs the credentials of the owner of that + snap set in the environment variables SPREAD_STORE_USER and SPREAD_STORE_PASSWORD, if + they are not present then only the negative check (private snap does not show up in + the find results without specifying private search or without the owner logged) is + performed. + +restore: | + snap logout || true + +execute: | + echo "When a snap is private it doesn't show up in the find without login and without specifying private search" + ! snap find test-snapd-private | MATCH "test-snapd-private +[0-9]+\.[0-9]+" + + echo "When a snap is private it doesn't show up in the find --private results without login" + ! snap find test-snapd-private --private | MATCH "test-snapd-private +[0-9]+\.[0-9]+" + + echo "Given account store credentials are available" + # we don't have expect available on ubuntu-core, so the authenticated check need to be skipped on those systems + if [ ! -z "$SPREAD_STORE_USER" ] && [ ! -z "$SPREAD_STORE_PASSWORD" ] && [[ ! "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + echo "And the user has logged in" + expect -f successful_login.exp + + echo "Then a private snap belonging to that user shows up in the find results and nothing else" + result=$(snap find test-snapd-private2 --private) + echo "$result" | MATCH "test-snapd-private2 +[0-9]+\.[0-9]+" + echo "$result" | MATCH -v "test-snapd-private +[0-9]+\.[0-9]+" + echo "$result" | MATCH -v "test-snapd-public +[0-9]+\.[0-9]+" + + echo "And searching for private snaps shows all of them and anything else" + result=$(snap find --private) + echo "$result" | MATCH "test-snapd-private2 +[0-9]+\.[0-9]+" + echo "$result" | MATCH "test-snapd-private +[0-9]+\.[0-9]+" + echo "$result" | MATCH -v "test-snapd-public +[0-9]+\.[0-9]+" + + echo "And searching for public snaps does not show private ones" + result=$(snap find test-snapd) + echo "$result" | MATCH -v "test-snapd-private2 +[0-9]+\.[0-9]+" + echo "$result" | MATCH -v "test-snapd-private +[0-9]+\.[0-9]+" + echo "$result" | MATCH "test-snapd-public +[0-9]+\.[0-9]+" + fi diff --git a/tests/main/generic-classic-reg/task.yaml b/tests/main/generic-classic-reg/task.yaml new file mode 100644 index 00000000..fb7321ed --- /dev/null +++ b/tests/main/generic-classic-reg/task.yaml @@ -0,0 +1,26 @@ +summary: | + Ensure device initialisation registration works with the fallback + generic/generi-classic model and we have a serial and can acquire + a session macaroon +systems: [-ubuntu-core-16-*] +execute: | + echo "Wait for device initialisation to have been done" + # this was in fact triggered around when core was installed + while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done + + echo "We have a model assertion" + snap known model|MATCH "series: 16" + + if ! snap known model|grep "brand-id: generic" ; then + echo "Not a generic model. Skipping." + exit 0 + fi + + echo "Check we have a serial" + snap known serial|MATCH "authority-id: generic" + snap known serial|MATCH "brand-id: generic" + snap known serial|MATCH "model: generic-classic" + + echo "Make sure we could acquire a session macaroon" + snap find pc + MATCH '"session-macaroon":"[^"]' < /var/lib/snapd/state.json diff --git a/tests/main/help/task.yaml b/tests/main/help/task.yaml new file mode 100644 index 00000000..138fe502 --- /dev/null +++ b/tests/main/help/task.yaml @@ -0,0 +1,13 @@ +summary: Check commands help +systems: [+fedora-*] +environment: + CMD/abort: abort + CMD/changes: changes + CMD/find: find + CMD/install: install + CMD/interfaces: interfaces + CMD/remove: remove +execute: | + echo "Checking help for command $CMD" + expected="(?s)Usage:\n snap \[OPTIONS\] $CMD.*?\n\nThe $CMD command .*?\nHelp Options:\n -h, --help +Show this help message\n.*?" + snap $CMD --help | grep -Pzq "$expected" diff --git a/tests/main/i18n/task.yaml b/tests/main/i18n/task.yaml new file mode 100644 index 00000000..a8f2a5a8 --- /dev/null +++ b/tests/main/i18n/task.yaml @@ -0,0 +1,26 @@ +summary: Test that i18n works + +execute: | + # The snapd deb from the archive does not contain .mo files, those + # are stripped out by the langpack buildd stuff and put into the + # the various langpacks. + # Therefore this test only makes sense when we build snapd from + # the local source. When running against an official snapd deb + # or against the core we will not see translations + if [ ! -f /usr/share/locale/de/LC_MESSAGES/snappy.mo ]; then + echo "SKIP: No mo files for snapd available" + exit 0 + fi + + echo "Ensure that i18n works" + LANG=de snap changes everything | MATCH "Ja, ja, allerdings." + + echo "Basic smoke test to ensure no locale causes crashes nor warnings" + for p in /usr/share/locale/*; do + out=$( LANG=$(basename $p) snap 2>&1 >/dev/null ) + if [ -n "$out" ]; then + echo "$p" + echo "$out" + exit 1 + fi + done diff --git a/tests/main/install-cache/task.yaml b/tests/main/install-cache/task.yaml new file mode 100644 index 00000000..fabece83 --- /dev/null +++ b/tests/main/install-cache/task.yaml @@ -0,0 +1,13 @@ +summary: Check that download caching works + +execute: | + snap install test-snapd-tools + snap remove test-snapd-tools + snap install test-snapd-tools + for i in $(seq 10); do + if journalctl -u snapd | MATCH "using cache for .*/test-snapd-tools.*\.snap"; then + break + fi + sleep 1 + done + journalctl -u snapd | MATCH "using cache for .*/test-snapd-tools.*\.snap" diff --git a/tests/main/install-errors/task.yaml b/tests/main/install-errors/task.yaml new file mode 100644 index 00000000..85facb23 --- /dev/null +++ b/tests/main/install-errors/task.yaml @@ -0,0 +1,63 @@ +summary: Checks for cli errors installing snaps + +systems: [-ubuntu-core-16-*] + +environment: + SNAP_NAME: test-snapd-tools + # Ensure that running purely from the deb (without re-exec) works + # correctly + SNAP_REEXEC/noreexec: 0 + SNAP_REEXEC/withreexec: 1 + +prepare: | + echo "Given a snap with a failing command is installed" + . $TESTSLIB/snaps.sh + install_local $SNAP_NAME + +execute: | + echo "Install unexisting snap prints error" + if snap install unexisting.canonical; then + echo "Installing unexisting snap should fail" + exit 1 + fi + + echo "============================================" + + echo "Install without snap name shows error" + if snap install; then + echo "Installing without snap name should fail" + exit 1 + fi + + echo "============================================" + + echo "Install points to sudo when not authenticated" + if su - -c "snap install $SNAP_NAME" test 2>install.output; then + echo "Unauthenticated install should fail" + exit 1 + fi + MATCH "try with sudo" < install.output + + echo "============================================" + + echo "Calling a failing command from a snap should fail" + if test-snapd-tools.fail; then + echo "Failing snap commands should keep failing after installed" + exit 1 + fi + + echo "============================================" + + echo "Install a snap that is already installed shows a message" + echo "but does "exit 0" (LP: #1622782)" + snap install $SNAP_NAME 2> stderr.out + MATCH "snap \"$SNAP_NAME\" is already installed" < stderr.out + + echo "============================================" + + echo "Install ubuntu-core" + if snap install ubuntu-core 2> stderr.out; then + echo "Installing ubuntu-core should fail" + exit 1 + fi + MATCH 'cannot install "ubuntu-core", please use "core" instead' < stderr.out diff --git a/tests/main/install-refresh-remove-hooks/task.yaml b/tests/main/install-refresh-remove-hooks/task.yaml new file mode 100644 index 00000000..3e711eba --- /dev/null +++ b/tests/main/install-refresh-remove-hooks/task.yaml @@ -0,0 +1,76 @@ +summary: Check install, remove and pre-refresh/post-refresh hooks. + +environment: + REMOVE_HOOK_FILE: "$HOME/remove-hook-executed" + +restore: | + rm -f $REMOVE_HOOK_FILE + +execute: | + . $TESTSLIB/snaps.sh + install_local snap-hooks + + echo "Verify configuration value with snap get" + snap get snap-hooks installed | MATCH 1 + snap get snap-hooks foo | MATCH bar + + echo "Verify that pre-refresh hook was not executed" + if snap get snap-install-hooks prerefreshed; then + echo "'prerefreshed' config value not expected on first install" + exit 1 + fi + + echo "Verify that post-refresh hook was not executed" + if snap get snap-install-hooks postrefreshed; then + echo "'postrefreshed' config value not expected on first install" + exit 1 + fi + + echo "Verify that install hook is run only once" + snap set snap-hooks installed=2 + install_local snap-hooks + snap get snap-hooks installed | MATCH 2 + + echo "Verify that pre-refresh hook was executed" + snap get snap-hooks prerefreshed | MATCH "pre-refresh at revision x1" + + echo "Verify that post-refresh hook was executed" + snap get snap-hooks postrefreshed | MATCH "post-refresh at revision x2" + + snap connect snap-hooks:home + + echo "Verify that remove hook is not executed when removing single revision" + snap set snap-hooks exitcode=0 + snap remove --revision=x1 snap-hooks + if test -f $REMOVE_HOOK_FILE; then + echo "Remove hook was executed. It shouldn't." + exit 1 + fi + + echo "Verify that remove hook is executed" + snap set snap-hooks exitcode=0 + snap remove snap-hooks + if ! test -f $REMOVE_HOOK_FILE; then + echo "Remove hook was not executed" + exit 1 + fi + + echo "Installing a snap with hooks again" + rm -f "$REMOVE_HOOK_FILE" 2>&1 > /dev/null + install_local snap-hooks + snap connect snap-hooks:home + + echo "Forcing remove script to fail" + snap set snap-hooks exitcode=1 + snap remove snap-hooks + EXITCODE_VALUE=$(cat $REMOVE_HOOK_FILE) + if test "x$EXITCODE_VALUE" != "x1"; then + echo "Remove hook was not executed" + exit 1 + fi + + echo "Installing a snap with broken install hook aborts the installation" + if install_local snap-hook-broken; then + echo "Expected installation to fail" + exit 1 + fi diff --git a/tests/main/install-remove-multi/task.yaml b/tests/main/install-remove-multi/task.yaml new file mode 100644 index 00000000..eeccd0df --- /dev/null +++ b/tests/main/install-remove-multi/task.yaml @@ -0,0 +1,12 @@ +summary: Check that install/remove of multiple snaps works + +execute: | + echo "Install multiple snaps from the store" + snap install test-snapd-tools test-snapd-control-consumer + snap list | MATCH test-snapd-tools + snap list | MATCH test-snapd-control-consumer + + echo "Remove of multiple snaps works" + snap remove test-snapd-tools test-snapd-control-consumer + snap list | MATCH -v test-snapd-tools + snap list | MATCH -v test-snapd-control-consumer diff --git a/tests/main/install-sideload/task.yaml b/tests/main/install-sideload/task.yaml new file mode 100644 index 00000000..a7ef9113 --- /dev/null +++ b/tests/main/install-sideload/task.yaml @@ -0,0 +1,77 @@ +summary: Checks for snap sideload install + +prepare: | + for snap in basic test-snapd-tools basic-desktop test-snapd-devmode + do + snap pack $TESTSLIB/snaps/$snap + done + +environment: + # Ensure that running purely from the deb (without re-exec) works + # correctly + SNAP_REEXEC/reexec0: 0 + SNAP_REEXEC/reexec1: 1 + +restore: | + for snap in basic test-snapd-tools basic-desktop + do + rm -f ./${snap}_1.0_all.snap + done + +execute: | + echo "Sideloaded snap shows status" + expected="(?s)basic 1.0 installed\n\ + .*" + snap install --dangerous ./basic_1.0_all.snap | grep -Pzq "$expected" + + echo "Sideloaded snap with (deprecated) --force-dangerous option" + snap remove basic + snap install --force-dangerous ./basic_1.0_all.snap | grep -Pzq "$expected" + + echo "Sideloaded snap executes commands" + snap install --dangerous ./test-snapd-tools_1.0_all.snap + test-snapd-tools.success + [ "$(test-snapd-tools.echo Hello World)" = "Hello World" ] + + . $TESTSLIB/dirs.sh + + echo "Sideload desktop snap" + snap install --dangerous ./basic-desktop_1.0_all.snap + expected="\[Desktop Entry\]\n\ + Name=Echo\n\ + Comment=It echos stuff\n\ + Exec=env BAMF_DESKTOP_FILE_HINT=/var/lib/snapd/desktop/applications/basic-desktop_echo.desktop $SNAP_MOUNT_DIR/bin/basic-desktop.echo\n" + cat /var/lib/snapd/desktop/applications/basic-desktop_echo.desktop | grep -Pzq "$expected" + + echo "Sideload devmode snap fails without flags" + expected="requires devmode or confinement override" + ( snap install --dangerous ./test-snapd-devmode_1.0_all.snap 2>&1 || true ) | grep -Pzq "$expected" + + echo "Sideload devmode snap succeeds with --devmode" + expected="test-snapd-devmode 1.0 installed" + snap install --devmode ./test-snapd-devmode_1.0_all.snap | grep -Pq "$expected" + expected="^test-snapd-devmode +.* +devmode" + snap list | grep -Pq "$expected" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "Sideload devmode snap succeeds with --jailmode" + expected="test-snapd-devmode 1.0 installed" + snap install --dangerous --jailmode ./test-snapd-devmode_1.0_all.snap | grep -Pq "$expected" + expected="^test-snapd-devmode +.* +jailmode" + snap list | grep -Pq "$expected" + fi + + echo "Sideload devmode snap fails with both --devmode and --jailmode" + expected="cannot use devmode and jailmode flags together" + ( snap install --devmode --jailmode ./test-snapd-devmode_1.0_all.snap 2>&1 || true ) | grep -Pzq "$expected" + + echo "Sideload a second time succeeds" + snap install --dangerous ./test-snapd-tools_1.0_all.snap + test-snapd-tools.success + + # TODO: check we copy the data directory over + + echo "Remove --revision works" + snap remove --revision x1 test-snapd-tools + test-snapd-tools.success + test ! -d $SNAP_MOUNT_DIR/test-snapd-tools/x1 diff --git a/tests/main/install-snaps/task.yaml b/tests/main/install-snaps/task.yaml new file mode 100644 index 00000000..469719cb --- /dev/null +++ b/tests/main/install-snaps/task.yaml @@ -0,0 +1,110 @@ +summary: Check install popular snaps + +details: | + This test is intended to install some popular snaps from + different channels. The idea is to detect any problem + installing that are currently published and some of them + with many revisions. + The execution of this test is made on the nightly build. + +manual: true + +environment: + # High Profile + SNAP/azurecli: azure-cli + SNAP/awscli: aws-cli + SNAP/heroku: heroku + SNAP/hiri: hiri + SNAP/kubectl: kubectl + SNAP/rocketchatserver: rocketchat-server + # Selected from recent insights posts + SNAP/corebird: corebird + SNAP/gitterdesktop: gitter-desktop + SNAP/helm: helm + SNAP/mattermostdesktop: mattermost-desktop + SNAP/mentaplexmediaserver: menta-plexmediaserver + SNAP/openspades: openspades + SNAP/pintown: pin-town + SNAP/postgresql10: postgresql10 + SNAP/storjshare: storjshare + SNAP/slackterm: slack-term + SNAP/vectr: vectr + SNAP/wekan: wekan + SNAP/wormhole: wormhole + # Featured snaps in Ubuntu Software + SNAP/anboxinstaller: anbox-installer + SNAP/lxd: lxd + # Top non canonical snaps + SNAP/atom: atom + SNAP/discord: discord + SNAP/docker: docker + SNAP/etcd: etcd + SNAP/geocoder: geocoder + SNAP/gimp: gimp + SNAP/huggle: huggle + SNAP/hugo: hugo + SNAP/ia: ia + SNAP/kurly: kurly + SNAP/micro: micro + SNAP/nikola: nikola + SNAP/parity: parity + SNAP/paritybitcoin: parity-bitcoin + SNAP/remmina: remmina + SNAP/telegramsergiusens: telegram-sergiusens + SNAP/zeronet: zeronet + SNAP/zeronetjs: zeronet-js + # Top canonical snaps + SNAP/bare: bare + SNAP/bluez: bluez + SNAP/conjureup: conjure-up + SNAP/gedit: gedit + SNAP/go: go + SNAP/juju: juju + SNAP/neutron: neutron + SNAP/nova: nova + SNAP/snapcraft: snapcraft + SNAP/solc: solc + SNAP/vault: vault + +prepare: | + cp /etc/systemd/system/snapd.service.d/local.conf /etc/systemd/system/snapd.service.d/local.conf.bak + sed 's/SNAPD_CONFIGURE_HOOK_TIMEOUT=.*s/SNAPD_CONFIGURE_HOOK_TIMEOUT=180s/g' -i /etc/systemd/system/snapd.service.d/local.conf + systemctl daemon-reload + systemctl restart snapd.socket + +restore: | + mv /etc/systemd/system/snapd.service.d/local.conf.bak /etc/systemd/system/snapd.service.d/local.conf + systemctl daemon-reload + systemctl restart snapd.socket + +execute: | + . "$TESTSLIB/snaps.sh" + + CHANNELS="stable candidate beta edge" + for CHANNEL in $CHANNELS; do + if ! CHANNEL_INFO="$(snap info $SNAP | grep " $CHANNEL: ")"; then + echo "Snap $SNAP not found" + exit + fi + if echo $CHANNEL_INFO | MATCH "$CHANNEL:.*–"; then + continue + fi + + if echo $CHANNEL_INFO | MATCH "$CHANNEL:.*classic"; then + if is_classic_confinement_supported; then + snap install $SNAP --$CHANNEL --classic + else + echo "The snap $SNAP requires classic confinement which is not supported yet" + exit + fi + elif echo $CHANNEL_INFO | MATCH "$CHANNEL:.*jailmode"; then + snap install $SNAP --$CHANNEL --jailmode + elif echo $CHANNEL_INFO | MATCH "$CHANNEL:.*devmode"; then + snap install $SNAP --$CHANNEL --devmode + else + snap install $SNAP --$CHANNEL + fi + break + done + + snap list | MATCH $SNAP diff --git a/tests/main/install-socket-activation/task.yaml b/tests/main/install-socket-activation/task.yaml new file mode 100644 index 00000000..68005251 --- /dev/null +++ b/tests/main/install-socket-activation/task.yaml @@ -0,0 +1,17 @@ +summary: Check install of a snap with socket activation + +details: | + This installs a snap which define sockets for systemd socket activation. + +prepare: | + snap pack $TESTSLIB/snaps/socket-activation + snap install --dangerous socket-activation_1.0_all.snap + +restore: | + rm -f socket-activation_1.0_all.snap + systemctl daemon-reload + +execute: | + [ -f /etc/systemd/system/snap.socket-activation.sleep-daemon.sock.socket ] + [ -S /var/snap/socket-activation/common/socket ] + diff --git a/tests/main/install-store-laaaarge/task.yaml b/tests/main/install-store-laaaarge/task.yaml new file mode 100644 index 00000000..a47f6106 --- /dev/null +++ b/tests/main/install-store-laaaarge/task.yaml @@ -0,0 +1,16 @@ +summary: snap install a large snap from the store (bigger than tmpfs) + +prepare: | + systemctl stop snapd.{service,socket} + mount -t tmpfs -o rw,nosuid,nodev,size=4 none /tmp + systemctl start snapd.{socket,service} + +restore: | + systemctl stop snapd.{service,socket} + umount /tmp || true + systemctl start snapd.{socket,service} + +execute: | + # test-snapd-tools is about 8k, tmpfs is 4k :-) + snap install test-snapd-tools + snap remove test-snapd-tools diff --git a/tests/main/install-store/task.yaml b/tests/main/install-store/task.yaml new file mode 100644 index 00000000..7920ab21 --- /dev/null +++ b/tests/main/install-store/task.yaml @@ -0,0 +1,41 @@ +summary: Checks for special cases of snap install from the store + +systems: [ubuntu-*] + +environment: + SNAP_NAME: test-snapd-tools + DEVMODE_SNAP: test-snapd-devmode + # Ensure that running purely from the deb (without re-exec) works + # correctly + SNAP_REEXEC/reexec0: 0 + SNAP_REEXEC/reexec1: 1 + +execute: | + echo "Install from different channels" + expected="(?s)$SNAP_NAME .* from 'canonical' installed\n" + for channel in edge beta candidate stable + do + snap install $SNAP_NAME --channel=$channel | grep -Pzq "$expected" + snap remove $SNAP_NAME + done + + echo "Install non-devmode snap with devmode option" + expected="(?s)$SNAP_NAME .* from 'canonical' installed\n" + snap install $SNAP_NAME --devmode | grep -Pzq "$expected" + + echo "Install devmode snap without devmode option" + expected="repeat the command including --devmode" + ( snap install --channel beta $DEVMODE_SNAP 2>&1 && exit 1 || true ) | MATCH -z "${expected// /[[:space:]]+}" + + echo "Install devmode snap from stable" + expected="snap \"${DEVMODE_SNAP}\" not found" + actual=$(snap install --devmode $DEVMODE_SNAP 2>&1 && exit 1 || true) + echo "$actual" | grep -Pzq "$expected" + + echo "Install devmode snap from beta with devmode option" + expected="(?s)$DEVMODE_SNAP .*" + actual=$(snap install --channel beta --devmode $DEVMODE_SNAP) + echo "$actual" | grep -Pzq "$expected" + + echo "Install a snap that contains bash-completion scripts" + snap install --edge test-snapd-complexion diff --git a/tests/main/interfaces-account-control/task.yaml b/tests/main/interfaces-account-control/task.yaml new file mode 100644 index 00000000..b308cf07 --- /dev/null +++ b/tests/main/interfaces-account-control/task.yaml @@ -0,0 +1,30 @@ +summary: Check that is possible to handle user accounts + +details: | + This test makes sure that a snap using the account-control interface + can handle the user accounts properly. + +systems: [ubuntu-core-16-64] + +prepare: | + echo "Given a snap declaring a plug on account-control is installed" + . $TESTSLIB/snaps.sh + install_local account-control-consumer + + echo "And the account-control plug is connected" + snap connect account-control-consumer:account-control + +restore: | + echo "Ensure alice is gone from the system" + for f in /var/lib/extrausers/*; do + sed -i '/^alice:/d' $f + done + +execute: | + . $TESTSLIB/dirs.sh + + $SNAP_MOUNT_DIR/bin/account-control-consumer.useradd --extrausers alice + echo alice:password | $SNAP_MOUNT_DIR/bin/account-control-consumer.chpasswd + + # User deletion is unsupported yet on Core: https://bugs.launchpad.net/ubuntu/+source/shadow/+bug/1659534 + # $SNAP_MOUNT_DIR/bin/account-control-consumer.userdel --extrausers alice diff --git a/tests/main/interfaces-alsa/task.yaml b/tests/main/interfaces-alsa/task.yaml new file mode 100644 index 00000000..863b575b --- /dev/null +++ b/tests/main/interfaces-alsa/task.yaml @@ -0,0 +1,100 @@ +summary: Ensure that the alsa interface works. + +# Spread system for Fedora, openSUSE doesn't seem to provide any /dev/snd entries +systems: [-fedora-*, -opensuse-*] + +details: | + The alsa interface allows connected plugs to access raw ALSA devices. + + A snap which defines a alsa plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to + be reconnected. + + A snap declaring a plug on this interface must be able to access to raw ALSA + devices, for this test we check the low level security rules by creating + specific devices under /dev/snd, a cgroup if needed and the ALSA state dir, + exercising in each caase the read or read-write permissions that must be in + place. + +prepare: | + echo "Given a snap declaring a plug on the alsa interface is installed" + . $TESTSLIB/snaps.sh + install_generic_consumer alsa + +restore: | + rm -f *.error /dev/snd/mysnd-dev + +execute: | + CONNECTED_PATTERN=":alsa +generic-consumer" + DISCONNECTED_PATTERN="\- +generic-consumer:alsa" + + echo "The plug is not connected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "===================================" + + echo "When the plug is connected" + snap connect generic-consumer:alsa + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to access snd devices" + generic-consumer.cmd touch /dev/snd/mysnd-dev + echo "mysnd-dev-content" | tee /dev/snd/mysnd-dev + generic-consumer.cmd cat /dev/snd/mysnd-dev | MATCH mysnd-dev-content + generic-consumer.cmd rm /dev/snd/mysnd-dev + + echo "And the snap is able to access the related udev data" + if [ ! -f /run/udev/data/c116:0 ]; then + touch /run/udev/data/c116:0 + echo "myudevdata-content" | tee /run/udev/data/c116:0 + generic-consumer.cmd cat /run/udev/data/c116:0 | MATCH myudevdata-content + rm /run/udev/data/c116:0 + fi + + # TODO: extend test to check /var/lib/alsa with plug connected when + # https://bugs.launchpad.net/snapd/+bug/1694281 is fixed + + if [ "$(snap debug confinement)" = partial ] ; then + echo "Do not execute checks with disconnected plug on systems where confinement doesn't work" + exit 0 + fi + + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect generic-consumer:alsa + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "The snap is not able to access snd devices" + if generic-consumer.cmd touch /dev/snd/mysnd-dev 2>snd-create.error; then + echo "Create snd device with disconnected plug should fail" + exit 1 + fi + MATCH "Permission denied" ./snd-create.error + touch /dev/snd/mysnd-dev + echo "mysnd-content" | tee /dev/snd/mysnd-dev + if generic-consumer.cmd cat /dev/snd/mysnd-dev 2>snd-read.error; then + echo "Read snd device with disconnected plug should fail" + exit 1 + fi + MATCH "Permission denied" ./snd-read.error + if generic-consumer.cmd rm /dev/snd/mysnd-dev 2>snd-del.error; then + echo "Delete snd device with disconnected plug should fail" + exit 1 + fi + MATCH "Permission denied" ./snd-del.error + + echo "And the snap is not able to access the related udev data" + if [ ! -f /run/udev/data/c116:0 ]; then + touch /run/udev/data/c116:0 + echo "myudevdata-content" | tee /run/udev/data/c116:0 + if generic-consumer.cmd cat /run/udev/data/c116:0 2>udevdata-read.error; then + echo "Read udev data file with disconnected plug should fail" + exit 1 + fi + MATCH "Permission denied" ./udevdata-read.error + rm /run/udev/data/c116:0 + fi + + # TODO: extend test to check /var/lib/alsa with plug disconnected when + # https://bugs.launchpad.net/snapd/+bug/1694281 is fixed diff --git a/tests/main/interfaces-autopilot-introspection/task.yaml b/tests/main/interfaces-autopilot-introspection/task.yaml new file mode 100644 index 00000000..424211b6 --- /dev/null +++ b/tests/main/interfaces-autopilot-introspection/task.yaml @@ -0,0 +1,85 @@ +summary: Ensure that the autopilot-introspection interface works + +details: | + The autopilot-intrspection interface allows an application to be introspected + and export its ui status over DBus. + + The test uses an snap that declares a plug on autopilot-intrsopection, it + needs to request a dbus name on start so that its state can be queried. + +systems: [-ubuntu-core-16-*] + +prepare: | + . "$TESTSLIB/dirs.sh" + + echo "Given a snap declaring an autopilot-intrspection plug in installed" + snap install --edge test-snapd-autopilot-consumer + + echo "And the provider dbus loop is started" + . "$TESTSLIB/dbus.sh" + start_dbus_unit $SNAP_MOUNT_DIR/bin/test-snapd-autopilot-consumer.provider + +restore: | + rm -f *.error + . "$TESTSLIB/dbus.sh" + stop_dbus_unit + +execute: | + . "$TESTSLIB/dirs.sh" + + dbus_send(){ + local method="$1" + echo $(dbus-send --print-reply --dest=com.canonical.Autopilot.Introspection /com/canonical/Autopilot/Introspection com.canonical.Autopilot.Introspection.${method}) + } + + CONNECTED_PATTERN=":autopilot-introspection +test-snapd-autopilot-consumer" + DISCONNECTED_PATTERN="^\- +test-snapd-autopilot-consumer:autopilot-introspection" + + export $(cat dbus.env) + + echo "Then the plug is disconnected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "When the plug is connected" + snap connect test-snapd-autopilot-consumer:autopilot-introspection + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the dbus name is properly reserved and the snap app version can be introspected" + + for i in $(seq 10); do + if ! dbus_send GetVersion | MATCH "my-ap-version"; then + sleep 1 + else + break + fi + done + $SNAP_MOUNT_DIR/bin/test-snapd-autopilot-consumer.consumer GetVersion | MATCH "my-ap-version" + + echo "And the snap app state can be intrsopected" + $SNAP_MOUNT_DIR/bin/test-snapd-autopilot-consumer.consumer GetState | MATCH "my-ap-state" + + if [ "$(snap debug confinement)" = none ]; then + exit 0 + fi + + echo "=================================" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "When the plug is disconnected" + snap disconnect test-snapd-autopilot-consumer:autopilot-introspection + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap version is not introspectable" + if $SNAP_MOUNT_DIR/bin/test-snapd-autopilot-consumer.consumer GetVersion 2>${PWD}/getversion.error ; then + echo "Expected permission error trying to introspect version with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < getversion.error + + echo "And the snap state is not introspectable" + if $SNAP_MOUNT_DIR/bin/test-snapd-autopilot-consumer.consumer GetState 2>${PWD}/getstate.error; then + echo "Expected permission error trying to introspect state with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < getstate.error + fi diff --git a/tests/main/interfaces-avahi-observe/task.yaml b/tests/main/interfaces-avahi-observe/task.yaml new file mode 100644 index 00000000..ac2df698 --- /dev/null +++ b/tests/main/interfaces-avahi-observe/task.yaml @@ -0,0 +1,52 @@ +summary: check that avahi-observe interface works + +systems: [-ubuntu-core-*, -fedora-*, -opensuse-*] + +prepare: | + echo "Given a snap with an avahi-observe interface plug is installed" + . $TESTSLIB/snaps.sh + install_generic_consumer avahi-observe,unity7 + + echo "And avahi-daemon is installed and configured" + . $TESTSLIB/pkgdb.sh + distro_install_package avahi-daemon + sed -i 's/^#enable-dbus=yes/enable-dbus=yes/' /etc/avahi/avahi-daemon.conf + if [[ "$SPREAD_SYSTEM" = ubuntu-14.04-* ]]; then + initctl reload-configuration + restart avahi-daemon + else + systemctl daemon-reload + systemctl restart avahi-daemon.{socket,service} + fi + +restore: | + rm -f *.error + . $TESTSLIB/pkgdb.sh + distro_purge_package avahi-daemon + distro_auto_remove_packages + +execute: | + CONNECTED_PATTERN=":avahi-observe +generic-consumer" + DISCONNECTED_PATTERN="^\- +generic-consumer:avahi-observe" + + avahi_dbus_call="dbus-send --system --print-reply --dest=org.freedesktop.Avahi / org.freedesktop.Avahi.Server.GetHostName" + + echo "Then the plug is disconnected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "And the snap is not able to access avahi provided info" + if generic-consumer.cmd $avahi_dbus_call 2>avahi.error; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + MATCH "org.freedesktop.DBus.Error.AccessDenied" < avahi.error + fi + + echo "When the plug is connected" + snap connect generic-consumer:avahi-observe + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to access avahi provided info" + hostname=$(cat /etc/hostname) + generic-consumer.cmd $avahi_dbus_call | MATCH "$hostname" diff --git a/tests/main/interfaces-bluetooth-control/task.yaml b/tests/main/interfaces-bluetooth-control/task.yaml new file mode 100644 index 00000000..c086cd99 --- /dev/null +++ b/tests/main/interfaces-bluetooth-control/task.yaml @@ -0,0 +1,60 @@ +summary: check that the bluetooth-control interface works + +# currently only enabled for the system that has bluetooth hardware (dragonboard) +systems: [ubuntu-core-16-arm-64] + +prepare: | + echo "Given a snap declaring a plug on bluetooth-control is installed" + . $TESTSLIB/snaps.sh + install_generic_consumer bluetooth-control + +restore: | + rm -f *.error version class control + +execute: | + CONNECTED_PATTERN=":bluetooth-control +generic-consumer" + DISCONNECTED_PATTERN="^\- +generic-consumer:bluetooth-control" + BTDEV="$(find /sys/devices/ -type d -name bluetooth)" + + echo "Then the plug is disconnected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "And the snap is not able to read usb" + if su -l -c "/snap/bin/generic-consumer.cmd cat /sys/bus/usb/drivers/btusb/module/version 2>${PWD}/btusb.error" test; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + cat btusb.error | MATCH "Permission denied" + + echo "And the snap is not able to read class" + if su -l -c "/snap/bin/generic-consumer.cmd cat /sys/class/bluetooth/*/name 2>${PWD}/btclass.error" test; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + cat btclass.error | MATCH "Permission denied" + + echo "And the snap is not able to read dev" + if su -l -c "/snap/bin/generic-consumer.cmd cat $BTDEV/*/power/control 2>${PWD}/btdev-read.error" test; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + cat btdev-read.error | MATCH "Permission denied" + fi + + echo "When the plug is connected" + snap connect generic-consumer:bluetooth-control + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to read usb" + cat /sys/bus/usb/drivers/btusb/module/version | tee version + # the next check is disabled because of https://bugs.launchpad.net/snapd/+bug/1698412 + # [ $(su -l -c "/snap/bin/generic-consumer.cmd cat /sys/bus/usb/drivers/btusb/module/version" test) = $(cat version) ] + + echo "And the snap is able to read class" + cat /sys/class/bluetooth/*/name | tee class + [ $(su -l -c "/snap/bin/generic-consumer.cmd cat /sys/class/bluetooth/*/name" test) = $(cat class) ] + + echo "And the snap is able to read dev" + cat $BTDEV/*/power/control | tee control + [ $(su -l -c "/snap/bin/generic-consumer.cmd cat $BTDEV/*/power/control" test) = $(cat control) ] diff --git a/tests/main/interfaces-bluez/task.yaml b/tests/main/interfaces-bluez/task.yaml new file mode 100644 index 00000000..04034404 --- /dev/null +++ b/tests/main/interfaces-bluez/task.yaml @@ -0,0 +1,19 @@ +summary: Ensure bluez interface works. + +details: | + The bluez interface allows the bluez service to run and clients to + communicate with it. + + This test verifies the the bluez snap from the store installs and + we can connect its slot and plug. + +environment: + SNAP_NAME: bluez + +execute: | + echo "Installing bluez snap from the store ..." + expected="(?s)$SNAP_NAME .* from 'canonical' installed\n" + snap install $SNAP_NAME | grep -Pzq "$expected" + + echo "Connecting bluez snap plugs/slots ..." + snap connect bluez:client bluez:service diff --git a/tests/main/interfaces-browser-support/task.yaml b/tests/main/interfaces-browser-support/task.yaml new file mode 100644 index 00000000..8e9495b9 --- /dev/null +++ b/tests/main/interfaces-browser-support/task.yaml @@ -0,0 +1,161 @@ +summary: Check that the browser-support interface works + +environment: + ALLOW_SANDBOX/allow: true + ALLOW_SANDBOX/disallow: false + OWNED_FILES: "/var/tmp/etilqs_test" + READABLE_FILES: + /run/udev/data/+platform:test + /etc/opt/chrome/test + READABLE_WITH_SANDBOX_FILES: + /run/udev/data/c1:1-test + /run/udev/data/c10:1-test + /run/udev/data/c13:1-test + /run/udev/data/c180:1-test + /run/udev/data/c4:1-test + /run/udev/data/c5:1-test + /run/udev/data/c7:1-test + /run/udev/data/+hid:test + /run/udev/data/+input:input1-test + /run/udev/data/c29:1-test + /run/udev/data/+backlight:test + /run/udev/data/+leds:test + /run/udev/data/c116:1-test + /run/udev/data/+sound:card1-test + /run/udev/data/c108:1-test + /run/udev/data/c189:1-test + /run/udev/data/c89:1-test + /run/udev/data/c81:1-test + /run/udev/data/+acpi:test + /run/udev/data/+hwmon:hwmon1-test + /run/udev/data/+i2c:test + +prepare: | + echo "Given a snap declaring a plug on browser-support with allow-sandbox set to $ALLOW_SANDBOX is installed" + cp -ar "$TESTSLIB/snaps/browser-support-consumer" . + sed "s/@ALLOW_SANDBOX@/$ALLOW_SANDBOX/" browser-support-consumer/meta/snap.yaml.in > browser-support-consumer/meta/snap.yaml + snap pack browser-support-consumer browser-support-consumer + snap install --dangerous browser-support-consumer/*.snap + rm -rf browser-support-consumer + +restore: | + rm -f *.error /var/tmp/test + for file in $OWNED_FILES $READABLE_FILES $READABLE_WITH_SANDBOX_FILES; do + rm -f $file + done + + for dir in $(cat created_dirs); do + rm -rf $dir + done + rm -f created_dirs + +execute: | + . "$TESTSLIB/dirs.sh" + + CONNECTED_PATTERN=":browser-support +browser-support-consumer" + DISCONNECTED_PATTERN="^\- +browser-support-consumer:browser-support" + + if [ "$ALLOW_SANDBOX" = "false" ]; then + echo "If allow-sandbox is false then the plug is connected by default" + snap interfaces | MATCH "$CONNECTED_PATTERN" + else + echo "If allow-sandbox is true then the plug is not connected by default" + ! snap interfaces | MATCH "$CONNECTED_PATTERN" + echo "Do connect it manually" + snap connect browser-support-consumer:browser-support + fi + + echo "And the snap is able to access tmp" + echo "test" > /var/tmp/test + su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd ls /var/tmp/" test | MATCH test + + echo "And the snap is able to access owned files" + for owned_file in $OWNED_FILES; do + echo "test" > "$owned_file" + chown test:12345 "$owned_file" + su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd cat $owned_file" test | MATCH test + su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd touch $owned_file" test + done + + echo "And the snap is able to access readable files" + for readable_file in $READABLE_FILES; do + parent_dir=$(dirname $readable_file) + if [ ! -d $parent_dir ]; then + if mkdir -p $parent_dir; then + echo "$parent_dir" >> created_dirs + else + echo "$parent_dir couldn't be created, write-only partition?" + continue + fi + fi + echo "test" > "$readable_file" + su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd cat $readable_file" test | MATCH test + done + + for readable_file in $READABLE_WITH_SANDBOX_FILES; do + parent_dir=$(dirname $readable_file) + if [ ! -d $parent_dir ]; then + mkdir -p $parent_dir + echo "$parent_dir" >> created_dirs + fi + echo "test" > "$readable_file" + done + + if [ "$ALLOW_SANDBOX" = "true" ]; then + for readable_file in $READABLE_WITH_SANDBOX_FILES; do + su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd cat $readable_file" test | MATCH test + done + fi + + if [ "$(snap debug confinement)" = strict ] ; then + echo "And the resources available with sandbox are not reachable without it" + if [ "$ALLOW_SANDBOX" = "false" ]; then + for readable_file in $READABLE_WITH_SANDBOX_FILES; do + if su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd cat $readable_file 2>${PWD}/readable-without-sandbox-read.err" test; then + echo "Expected error without sandbox didn't happen" + exit 1 + fi + MATCH "Permission denied" < readable-without-sandbox-read.err + done + fi + + echo "When the plug is disconnected" + snap disconnect browser-support-consumer:browser-support + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to access tmp" + if su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd ls /var/tmp/ 2>${PWD}/tmpdir-access.err" test; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + MATCH "Permission denied" < tmpdir-access.err + if su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd cat /var/tmp/etilqs_test 2>${PWD}/tmpfile-read.err" test; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + MATCH "Permission denied" < tmpfile-read.err + + for owned_file in $OWNED_FILES; do + if su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd cat $owned_file 2>${PWD}/owned-read.err" test; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + MATCH "Permission denied" < owned-read.err + done + for readable_file in $READABLE_FILES; do + if [ -f "$readable_file" ] && su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd cat $readable_file 2>${PWD}/readable-read.err" test; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + MATCH "Permission denied" < readable-read.err + done + if [ "$ALLOW_SANDBOX" = "true" ]; then + for readable_file in $READABLE_WITH_SANDBOX_FILES; do + if su -l -c "$SNAP_MOUNT_DIR/bin/browser-support-consumer.cmd cat $readable_file 2>${PWD}/readable-with-sandbox-read.err" test; then + echo "Expected error with disconnected plug didn't happen" + exit 1 + fi + MATCH "Permission denied" < readable-with-sandbox-read.err + done + fi + fi diff --git a/tests/main/interfaces-cli/task.yaml b/tests/main/interfaces-cli/task.yaml new file mode 100644 index 00000000..379431d0 --- /dev/null +++ b/tests/main/interfaces-cli/task.yaml @@ -0,0 +1,28 @@ +summary: Check the interfaces command + +environment: + SNAP_NAME: network-consumer + SNAP_FILE: ${SNAP_NAME}_1.0_all.snap + PLUG: network + +prepare: | + echo "Given a snap with the $PLUG plug is installed" + snap pack $TESTSLIB/snaps/$SNAP_NAME + snap install --dangerous $SNAP_FILE + +restore: | + rm -f $SNAP_FILE + +execute: | + expected="(?s)Slot +Plug\n\ + :$PLUG +$SNAP_NAME" + + echo "When the interfaces list is restricted by slot" + echo "Then only the requested slots are shown" + snap interfaces -i $PLUG | grep -Pzq "$expected" + + echo "===============================================" + + echo "When the interfaces list is restricted by slot and snap" + echo "Then only the requested slots are shown" + snap interfaces -i $PLUG $SNAP_NAME | grep -Pzq "$expected" diff --git a/tests/main/interfaces-content-empty-content-attr/task.yaml b/tests/main/interfaces-content-empty-content-attr/task.yaml new file mode 100644 index 00000000..e2179f14 --- /dev/null +++ b/tests/main/interfaces-content-empty-content-attr/task.yaml @@ -0,0 +1,41 @@ +summary: Ensure that the content sharing interface with defaults work. + +prepare: | + echo "Given a snap declaring a content sharing slot is installed" + snap install --edge test-snapd-content-slot-empty-content-attr + + echo "And a snap declaring a content sharing plug is installed" + snap install --edge test-snapd-content-plug-empty-content-attr + +execute: | + CONNECTED_PATTERN="test-snapd-content-slot-empty-content-attr:shared-content +test-snapd-content-plug-empty-content-attr" + DISCONNECTED_PATTERN="(?s).*?test-snapd-content-slot-empty-content-attr:shared-content +-.*?- +test-snapd-content-plug-empty-content-attr" + + echo "Then the snap is listed as connected" + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "And fstab files are created" + [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -gt 0 ] + + echo "And we can use the shared content" + test-snapd-content-plug-empty-content-attr.content-plug | grep "Some shared content" + + if [ "$(snap debug confinement)" = partial ]; then + exit 0 + fi + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-content-plug-empty-content-attr:shared-content test-snapd-content-slot-empty-content-attr:shared-content + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the fstab files are removed" + [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -eq 0 ] + + echo "When the plug is reconnected" + snap connect test-snapd-content-plug-empty-content-attr:shared-content test-snapd-content-slot-empty-content-attr:shared-content + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the fstab files are recreated" + [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -gt 0 ] diff --git a/tests/main/interfaces-content-mkdir-writable/task.yaml b/tests/main/interfaces-content-mkdir-writable/task.yaml new file mode 100644 index 00000000..dfb04d08 --- /dev/null +++ b/tests/main/interfaces-content-mkdir-writable/task.yaml @@ -0,0 +1,103 @@ +summary: check that snap-update-ns can create mkdir in $SNAP_{DATA,COMMON} +details: | + When snap-update-ns is invoked by snap-confine to construct a mount + namespace it will create missing directories for the mount target. + This will succeed in specific writable locations, such as $SNAP_DATA and + $SNAP_COMMON. The $SNAP location is read only but thanks to overalyfs it + too can be modified for the calling snap. The test is divided into variants + for one of each $SNAP, $SNAP_DATA and $SNAP_COMMON. There's a slight + variation for the $SNAP variable, see below for details. +environment: + PLUG/data: test-snapd-content-advanced-plug:data + PLUG/common: test-snapd-content-advanced-plug:common + PLUG/snap: test-snapd-content-advanced-plug:snap + SLOT/data: test-snapd-content-advanced-slot:data + SLOT/common: test-snapd-content-advanced-slot:common + SLOT/snap: test-snapd-content-advanced-slot:snap + VAR/data: SNAP_DATA + VAR/common: SNAP_COMMON + VAR/snap: SNAP +prepare: | + # Install a pair of snaps that both have two content interfaces as + # sub-directories of $SNAP_DATA and $SNAP_COMMON. + . $TESTSLIB/snaps.sh + install_local test-snapd-content-advanced-plug + install_local test-snapd-content-advanced-slot +execute: | + # Put our internal tools on PATH so that we can call snap-discard-ns easily. + . $TESTSLIB/dirs.sh + PATH=$LIBEXECDIR/snapd:$PATH + + # Skip the SNAP variant until the related branch is merged. + if [ "$VAR" = "SNAP" ]; then + echo "Poking holes in read-only space is not yet supported." + exit 0 + fi + + # Test that initially there are no mount points on the plug side (because + # nothing is connected). All of the plug side target directories are + # created dynamically when the corresponding content interface connects. + test-snapd-content-advanced-plug.sh -c "$(printf 'test ! -e $%s/target' "$VAR")" + + # Test that initially there are almost no mount sources on the slot side + # (again because nothing is connected yet). All of the slot side source + # directories are created upon connection. The only exception is the + # $SNAP/source directory that must be present at all times. + if [ "$VAR" != "SNAP" ]; then + test-snapd-content-advanced-slot.sh -c "$(printf 'test ! -e $%s/source' "$VAR")" + else + # The reason why this directory exists in $SNAP/source is that the snap + # simply always has it. The snap with the content plug cannot poke a + # hole that would be visible to the snap that holds the slot because + # both snaps see separate mount namespaces and the changes that they + # make in their own mount namespace are not propagated to each other. + # + # In result, the source of a content share, if placed in $SNAP + # somewhere, must exist in the snap and cannot be created dynamically. + test-snapd-content-advanced-slot.sh -c "$(printf 'test -d $%s/source' "$VAR")" + fi + + # Discard the namespaces, we want to do this because the code path when + # snap-update-ns is invoked from snapd is easier than the one when + # snap-confine invokes it to do the initial setup. We are testing the + # initial setup here. + snap-discard-ns test-snapd-content-advanced-plug + snap-discard-ns test-snapd-content-advanced-slot + + # Connect the plug to the slot. This should just write the mount profiles + # to disk as we just have discarded the namespaces so there is nothing to + # modify. + snap connect "$PLUG" "$SLOT" + + # Test that mount points are created automatically upon initialization of + # the namespace. This also tests apparmor confinement for snap-update-ns + test-snapd-content-advanced-plug.sh -c "$(printf 'test -d $%s/target' "$VAR")" + test-snapd-content-advanced-slot.sh -c "$(printf 'test -d $%s/source' "$VAR")" + + # Write some data into from the slot side. The $SNAP/source/canary file is + # always present and we cannot write to it anyway. + if [ "$VAR" != "SNAP" ]; then + test-snapd-content-advanced-slot.sh -c "$(printf 'touch $%s/source/canary' "$VAR")" + fi + + # Ensure that the bind mounts worked correctly by observing the data from plug side. + test-snapd-content-advanced-plug.sh -c "$(printf 'test -f $%s/target/canary' "$VAR")" + + # Without discarding the namespace disconnect the content interface. This + # should undo the bind mounts but keep the directories around. The canary + # file is thus no longer accessible but all the directories are in place + # because we never remove them explicitly. + snap disconnect "$PLUG" "$SLOT" + + if [ "$VAR" != "SNAP" ]; then + test-snapd-content-advanced-plug.sh -c "$(printf 'test -d $%s/target' "$VAR")" + else + test-snapd-content-advanced-plug.sh -c "$(printf 'test ! -d $%s/target' "$VAR")" + fi + test-snapd-content-advanced-plug.sh -c "$(printf 'test ! -e $%s/target/canary' "$VAR")" + test-snapd-content-advanced-slot.sh -c "$(printf 'test -d $%s/source' "$VAR")" + test-snapd-content-advanced-slot.sh -c "$(printf 'test -e $%s/source/canary' "$VAR")" + + # Re-connect the content interface. We should now see the data again. + snap connect "$PLUG" "$SLOT" + test-snapd-content-advanced-plug.sh -c "$(printf 'test -e $%s/target/canary' "$VAR")" diff --git a/tests/main/interfaces-content/task.yaml b/tests/main/interfaces-content/task.yaml new file mode 100644 index 00000000..91c186ba --- /dev/null +++ b/tests/main/interfaces-content/task.yaml @@ -0,0 +1,48 @@ +summary: Ensure that the content sharing interface works. + +details: | + The content-sharing interface interface allows a snap to access contents from + other snap + + A snap which defines the content sharing plug must be shown in the interfaces list. + The plug must be autoconnected on install and, as usual, must be able to be + reconnected. + +prepare: | + echo "Given a snap declaring a content sharing slot is installed" + snap install --edge test-snapd-content-slot + + echo "And a snap declaring a content sharing plug is installed" + snap install --edge test-snapd-content-plug + +execute: | + CONNECTED_PATTERN="test-snapd-content-slot:shared-content-slot +test-snapd-content-plug:shared-content-plug" + DISCONNECTED_PATTERN="(?s).*?test-snapd-content-slot:shared-content-slot +-.*?- +test-snapd-content-plug:shared-content-plug" + + echo "Then the snap is listed as connected" + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "And fstab files are created" + [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -gt 0 ] + + echo "And we can use the shared content" + test-snapd-content-plug.content-plug | grep "Some shared content" + + echo "And the current mount profile is the same as the desired mount profile" + diff -u /run/snapd/ns/snap.test-snapd-content-plug.fstab /var/lib/snapd/mount/snap.test-snapd-content-plug.fstab + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-content-plug:shared-content-plug test-snapd-content-slot:shared-content-slot + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the fstab files are removed" + [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -eq 0 ] + + echo "When the plug is reconnected" + snap connect test-snapd-content-plug:shared-content-plug test-snapd-content-slot:shared-content-slot + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the fstab files are recreated" + [ $(find /var/lib/snapd/mount -type f -name "*.fstab" | wc -l) -gt 0 ] diff --git a/tests/main/interfaces-cups-control/task.yaml b/tests/main/interfaces-cups-control/task.yaml new file mode 100644 index 00000000..aba2a5b8 --- /dev/null +++ b/tests/main/interfaces-cups-control/task.yaml @@ -0,0 +1,81 @@ +summary: Ensure that the cups interface works. + +# Default cups/cups-pdf configuration on these distributions isn't +# working yet without further tweaks. +systems: [-ubuntu-core-16-*, -opensuse-*, -fedora-*] + +details: | + The cups-control interface allows a snap to access the locale configuration. + + A snap which defines the cups-control plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to talk to the cups daemon. + The test snap used pulls the lpr functionality and installs a pdf printer. In order + to actually connect to the socket it needs to declare a plug on the network interface. + The test checks for the existence of a pdf file generated by the lpr command in the + snap. + +environment: + TEST_FILE: /var/snap/test-snapd-cups-control-consumer/current/test_file.txt + +prepare: | + echo "Given a snap declaring a cups plug is installed" + snap install test-snapd-cups-control-consumer + + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + # Not all distributions are starting the cups service directly after + # the package was installed. + echo "Enabling cups service in case it is not enabled" + if ! systemctl is-enabled cups ; then + # We can't use --now as this isn't supported by all distributions + systemctl enable cups + fi + echo "Starting cups service in case it is not active" + if ! systemctl is-active cups; then + systemctl start cups + fi + fi + +restore: | + rm -rf $HOME/PDF $TEST_FILE print.error + +debug: | + systemctl status cups || true + journalctl -u cpus || true + +execute: | + CONNECTED_PATTERN=":cups-control +test-snapd-cups-control-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-cups-control-consumer:cups-control" + + echo "Then it is not shown as connected" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "====================================" + + echo "When the plug is connected" + snap connect test-snapd-cups-control-consumer:cups-control + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap command is able to print files" + echo "Hello World" > $TEST_FILE + test-snapd-cups-control-consumer.lpr $TEST_FILE + while ! test -e $HOME/PDF/*.pdf; do sleep 1; done + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "====================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-cups-control-consumer:cups-control + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the snap command is not able to print files" + if test-snapd-cups-control-consumer.lpr $TEST_FILE 2>print.error; then + echo "Expected error with plug disconnected" + exit 1 + fi + grep -q "scheduler not responding" print.error diff --git a/tests/main/interfaces-dbus/task.yaml b/tests/main/interfaces-dbus/task.yaml new file mode 100644 index 00000000..5f9aa57c --- /dev/null +++ b/tests/main/interfaces-dbus/task.yaml @@ -0,0 +1,64 @@ +summary: Ensure that the dbus interface works + +details: | + The dbus interface allows owning a name on DBus public bus. + + The test uses two snaps, a provider declaring a dbus slot and a consumer + with a plug with the same attributes as the slot. The provider requests + a dbus name and, when the plug is connected, the consumer can call the + method exposed by the provider. + +environment: + DISPLAY: :0 + +systems: [-ubuntu-core-16-*] + +prepare: | + . "$TESTSLIB/dirs.sh" + + echo "Give a snap declaring a dbus slot in installed" + snap install --edge test-snapd-dbus-provider + + echo "And a snap declaring a matching dbus plug is installed" + snap install --edge test-snapd-dbus-consumer + + echo "And the provider dbus loop is started" + . "$TESTSLIB/dbus.sh" + start_dbus_unit $SNAP_MOUNT_DIR/bin/test-snapd-dbus-provider.provider + +restore: | + rm -f call.error + . "$TESTSLIB/dbus.sh" + stop_dbus_unit + +execute: | + CONNECTED_PATTERN="test-snapd-dbus-provider:dbus-test +test-snapd-dbus-consumer" + DISCONNECTED_PATTERN="^\- +test-snapd-dbus-consumer:dbus-test" + + export $(cat dbus.env) + + echo "Then the dbus name is properly reserved by the provider and the method is accessible" + while ! dbus-send --print-reply --dest=com.dbustest.HelloWorld /com/dbustest/HelloWorld com.dbustest.HelloWorld.SayHello | MATCH "hello world"; do + sleep 1 + done + + echo "And plug is disconnected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = partial ]; then + exit 0 + fi + + echo "And the consumer is not able to access the provided method" + if test-snapd-dbus-consumer.dbus-consumer 2>${PWD}/call.error; then + echo "Expected permission error calling dbus method with disconnected plug" + exit 1 + fi + cat call.error | MATCH "Permission denied" + + echo "When the plug is connected" + snap connect test-snapd-dbus-consumer:dbus-test test-snapd-dbus-provider:dbus-test + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the consumer is able to call the provided method" + test-snapd-dbus-consumer.dbus-consumer | MATCH "hello world" diff --git a/tests/main/interfaces-desktop/task.yaml b/tests/main/interfaces-desktop/task.yaml new file mode 100644 index 00000000..8cd50029 --- /dev/null +++ b/tests/main/interfaces-desktop/task.yaml @@ -0,0 +1,61 @@ +summary: Ensure that the desktop interface works. + +details: | + The desktop interface allows to access the different resources. + + The test-snapd-desktop snap checks files and dirs are accessible through the + desktop interface. + +systems: [-ubuntu-core-*] + +prepare: | + echo "Given the desktop snap is installed" + snap try $TESTSLIB/snaps/test-snapd-desktop + +execute: | + CONNECTED_PATTERN=":desktop +test-snapd-desktop" + DISCONNECTED_PATTERN="\- +test-snapd-desktop:desktop" + + dirs="/var/cache/fontconfig /usr/share/icons /usr/share/pixmaps" + files="/etc/xdg/user-dirs.conf /etc/xdg/user-dirs.defaults" + + echo "The plug is connected by default" + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to desktop files and directories" + test-snapd-desktop.check-files $files + test-snapd-desktop.check-dirs $dirs + + echo "The mount namespace contains shared font directories" + for d in /usr/share/fonts /usr/local/share/fonts /var/cache/fontconfig; do + if [ -d "$d" ]; then + cat /run/snapd/ns/snap.test-snapd-desktop.fstab | MATCH "$d" + test-snapd-desktop.check-dirs "$d" + fi + done + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-desktop:desktop + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to access the desktop files" + for file in $files; do + if test-snapd-desktop.check-files $file 2>${PWD}/call.error; then + echo "Expected permission error calling desktop with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < call.error + done + + echo "Then the snap is not able to access the desktop dirs" + for dir in $dirs; do + if test-snapd-desktop.check-dirs $dir 2>${PWD}/call.error; then + echo "Expected permission error calling desktop with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < call.error + done diff --git a/tests/main/interfaces-firewall-control/task.yaml b/tests/main/interfaces-firewall-control/task.yaml new file mode 100644 index 00000000..4e04a9a1 --- /dev/null +++ b/tests/main/interfaces-firewall-control/task.yaml @@ -0,0 +1,89 @@ +summary: Ensure that the firewall-control interface works. + +systems: [-fedora-*, -opensuse-*] + +details: | + The firewall-control interface allows a snap to configure the firewall. + + A snap which defines the firewall-control plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + For this test we use a snap that declares a plug on this interface and that adds and + removes iptables entries. With the plug connected the test checks that a rule to map + localhost to a given IP can be added by the snap, ensuring that a generic client can + access a generic service listening on localhost through the IP set up in the firewall + rule. + +environment: + PORT: 8081 + SERVICE_FILE: "./service.sh" + SERVICE_NAME: "test-service" + REQUEST_FILE: "./request.txt" + DESTINATION_IP: "172.26.0.15" + +prepare: | + echo "Given a snap declaring a plug on the firewall-control interface is installed" + snap pack $TESTSLIB/snaps/firewall-control-consumer + snap install --dangerous firewall-control-consumer_1.0_all.snap + + echo "And a service is listening" + printf "#!/bin/sh -e\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT -w 1; done" > $SERVICE_FILE + chmod a+x $SERVICE_FILE + . "$TESTSLIB/systemd.sh" + systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)" + + while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done + + echo "And we store a basic HTTP request" + cat > $REQUEST_FILE <firewall-create.error; then + echo "Expected permission error creating firewall rules with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < firewall-create.error diff --git a/tests/main/interfaces-framebuffer/task.yaml b/tests/main/interfaces-framebuffer/task.yaml new file mode 100644 index 00000000..621bb180 --- /dev/null +++ b/tests/main/interfaces-framebuffer/task.yaml @@ -0,0 +1,48 @@ +summary: Ensure that the framebuffer interface works. + +details: | + The framebuffer interface allows to access the /dev/fb* buffer files. + + The test uses the test-snapd-framebuffer snap to write/read from /dev/fb0. + + The test also checks the interface connection and disconnection works properly. + +prepare: | + snap try $TESTSLIB/snaps/test-snapd-framebuffer + +execute: | + CONNECTED_PATTERN=":framebuffer +test-snapd-framebuffer" + DISCONNECTED_PATTERN="\- +test-snapd-framebuffer:framebuffer" + + if [ ! -e /dev/fb0 ]; then + echo "Framebuffer not available on /dev/fb0" + exit 0 + fi + + echo "The plug is not connected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "When the interface is connected" + snap connect test-snapd-framebuffer:framebuffer + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to write in the framebuffer + test-snapd-framebuffer.write "123" + MATCH "123" < /dev/fb0 + + echo "Then the snap is able to read from the framebuffer + test-snapd-framebuffer.read + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-framebuffer:framebuffer + + echo "Then the snap is not able to access the framebuffer" + if test-snapd-framebuffer.write "123" 2>${PWD}/call.error; then + echo "Expected permission error trying to write the framebuffer" + exit 1 + fi + MATCH "Permission denied" < call.error diff --git a/tests/main/interfaces-fuse_support/task.yaml b/tests/main/interfaces-fuse_support/task.yaml new file mode 100644 index 00000000..74169edb --- /dev/null +++ b/tests/main/interfaces-fuse_support/task.yaml @@ -0,0 +1,61 @@ +summary: Ensure that the fuse-support interface works. + +# no support for fuse on 14.04 +systems: + # no support for fuse on 14.04 + - -ubuntu-14.04-64 + - -ubuntu-14.04-32 + +details: | + The fuse-support interface allows a snap to manage FUSE file systems. + + A snap which defines the fuse-support plug must be shown in the interfaces list. + The plug must be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to create a fuse filesystem + in a writable zone. The fuse-consumer test snap creates a readable file with a known + name and content in the mount point given to the command. + +environment: + MOUNT_POINT: /var/snap/test-snapd-fuse-consumer/current/mount_point + +prepare: | + echo "Given a snap declaring a fuse plug is installed" + snap install test-snapd-fuse-consumer + + echo "And a user writable mount point is created" + mkdir -p $MOUNT_POINT + +restore: | + umount $MOUNT_POINT || true + rm -rf $MOUNT_POINT fuse.error + +execute: | + CONNECTED_PATTERN=":fuse-support +test-snapd-fuse-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-fuse-consumer:fuse-support" + + echo "Then the fuse plug is not connected by default" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "Then the snap is not able to create a fuse file system" + if test-snapd-fuse-consumer.create $MOUNT_POINT 2>${PWD}/fuse.error; then + echo "Expected permission error creating fuse filesystem with disconnected plug" + exit 1 + fi + MATCH "Permission denied" fuse.error + + echo "===================================" + fi + + echo "When the plug is connected" + snap connect test-snapd-fuse-consumer:fuse-support + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to create a fuse filesystem" + test-snapd-fuse-consumer.create $MOUNT_POINT + # use "[f]oo" to search for "foo" in ps output without having to filter yourself out + PID=$(ps aux | awk '/[t]est-snapd.*create/{print $2}') + MATCH "Hello World!" < /proc/${PID}/root/$MOUNT_POINT/hello + kill -9 ${PID} diff --git a/tests/main/interfaces-hardware-observe/task.yaml b/tests/main/interfaces-hardware-observe/task.yaml new file mode 100644 index 00000000..43aa2e11 --- /dev/null +++ b/tests/main/interfaces-hardware-observe/task.yaml @@ -0,0 +1,49 @@ +summary: Ensure that the hardware-observe interface works. + +summary: | + The hardware-observe interface allows a snap to access hardware information. + + A snap which defines the hardware-observe plug must be shown in the interfaces list. + The plug must not be connected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to read files in /sys/{block,bus,class,devices} + +prepare: | + echo "Given a snap declaring a plug on the hardware-observe interface is installed" + snap pack $TESTSLIB/snaps/hardware-observe-consumer + snap install --dangerous hardware-observe-consumer_1.0_all.snap + +restore: | + rm -f hardware-observe-consumer_1.0_all.snap hw.error + +execute: | + CONNECTED_PATTERN=":hardware-observe +hardware-observe-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +hardware-observe-consumer:hardware-observe" + + echo "Then it is not connected by default" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "===================================" + + echo "When the plug is connected" + snap connect hardware-observe-consumer:hardware-observe + + echo "Then the snap is able to read hardware information" + hardware-observe-consumer.consumer + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect hardware-observe-consumer:hardware-observe + + echo "Then the snap is not able to read the hardware information" + if hardware-observe-consumer.consumer 2>hw.error; then + echo "Expected permission error accessing locale configuration with disconnected plug" + exit 1 + fi + grep -q "Permission denied" hw.error diff --git a/tests/main/interfaces-hardware-random-control/task.yaml b/tests/main/interfaces-hardware-random-control/task.yaml new file mode 100644 index 00000000..9503dbf7 --- /dev/null +++ b/tests/main/interfaces-hardware-random-control/task.yaml @@ -0,0 +1,55 @@ +summary: Ensure that the hardware-random-control interface works. + +summary: | + The hardware-observe interface allows a snap to access hardware-random information. + + A snap which access to the hardware-random information must be shown in the interfaces list. + The plug must not be connected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to read files in + /sys/class/misc/hw_random/{rng_available,rng_current} + +# Execution skipped on debian due to device /dev/hwrng not created by default +systems: [-debian-*] + +prepare: | + echo "Given a snap declaring a plug on the hardware-random-control interface is installed" + snap try $TESTSLIB/snaps/test-snapd-hardware-random-control + +restore: | + rm -f hw.error + +execute: | + CONNECTED_PATTERN=":hardware-random-control +test-snapd-hardware-random-control" + DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-hardware-random-control:hardware-random-control" + + echo "Then it is not connected by default" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "When the plug is connected" + snap connect test-snapd-hardware-random-control:hardware-random-control + + echo "Then the snap is able to read hardware random information" + test-snapd-hardware-random-control.check 2>hw.error + if MATCH "Permission denied" < hw.error; then + echo "Permission error accessing hardware random information" + exit 1 + fi + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-hardware-random-control:hardware-random-control + + echo "Then the snap is not able to read the hardware random information" + if test-snapd-hardware-random-control.check 2>hw.error; then + echo "Expected permission error accessing hardware control information with disconnected plug" + exit 1 + fi + grep -q "Permission denied" hw.error + + echo "And the snap is able to reconnect" + snap connect test-snapd-hardware-random-control:hardware-random-control diff --git a/tests/main/interfaces-home/task.yaml b/tests/main/interfaces-home/task.yaml new file mode 100644 index 00000000..6d4f9871 --- /dev/null +++ b/tests/main/interfaces-home/task.yaml @@ -0,0 +1,155 @@ +summary: Ensure that the home interface works. + +details: | + The home interface allows a snap to access non-hidden files in $HOME + + A snap which defines the home plug must be shown in the interfaces list. + The plug must be autoconnected on install for classic systems and disconnected + on all-snaps and, as usual, must be able to be reconnected. When connected + it must grant access to non hidden home files. + +environment: + SNAP_FILE: "home-consumer_1.0_all.snap" + CREATABLE_FILE: "$HOME/creatable" + READABLE_FILE: "$HOME/readable" + WRITABLE_FILE: "$HOME/writable" + HIDDEN_CREATABLE_FILE: "$HOME/.creatable" + HIDDEN_READABLE_FILE: "$HOME/.readable" + +prepare: | + echo "Given a snap declaring the home plug is installed" + snap pack $TESTSLIB/snaps/home-consumer + snap install --dangerous $SNAP_FILE + + echo "And there is a readable file in HOME" + echo ok > "$READABLE_FILE" + + echo "And there is a writable file in HOME" + echo ok > "$WRITABLE_FILE" + + echo "And there is a hidden readable file in HOME" + echo ok > "$HIDDEN_READABLE_FILE" + +restore: | + rm -f $SNAP_FILE $READABLE_FILE $WRITABLE_FILE $CREATABLE_FILE $HIDDEN_READABLE_FILE + +execute: | + CONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + :home +home-consumer" + DISCONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + - +home-consumer:home" + + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + echo "Then the snap is listed as disconnected" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "And the plug can be connected" + snap connect home-consumer:home + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + else + echo "Then the snap is listed as connected" + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect home-consumer:home + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the plug can be connected again" + snap connect home-consumer:home + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + fi + echo "============================================" + + echo "When the plug is connected" + snap connect home-consumer:home + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to read home files" + home-consumer.reader $READABLE_FILE | grep -Pqz ok + + echo "============================================" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "When the plug is disconnected" + snap disconnect home-consumer:home + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then snap can't read home files" + if home-consumer.reader $READABLE_FILE; then + echo "Home files shouldn't be readable" && exit 1 + fi + + echo "============================================" + fi + + echo "When the plug is connected" + snap connect home-consumer:home + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to append to home files" + home-consumer.writer "$WRITABLE_FILE" + cat "$WRITABLE_FILE" | grep -Pqz "ok\nok" + + echo "============================================" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "When the plug is disconnected" + snap disconnect home-consumer:home + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then snap can't append to home files" + if home-consumer.writer "$WRITABLE_FILE"; then + echo "Home files shouldn't be writable" && exit 1 + fi + + echo "============================================" + fi + + echo "When the plug is connected" + snap connect home-consumer:home + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to create home files" + home-consumer.writer "$CREATABLE_FILE" + cat "$CREATABLE_FILE" | grep -Pqz "ok" + + echo "============================================" + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect home-consumer:home + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then snap can't create home files" + if home-consumer.writer "$CREATABLE_FILE"; then + echo "It should be impossible to create home files" && exit 1 + fi + + echo "============================================" + + echo "When the plug is connected" + snap connect home-consumer:home + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is not able to read hidden home files" + if home-consumer.reader "$HIDDEN_READABLE_FILE"; then + echo "Hidden home files shouldn't be readable" && exit 1 + fi + + echo "============================================" + + echo "When the plug is connected" + snap connect home-consumer:home + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is not able to write hidden home files" + if home-consumer.writer "$HIDDEN_CREATABLE_FILE"; then + echo "It should be impossible to create hidden home files" && exit 1 + fi diff --git a/tests/main/interfaces-hooks/task.yaml b/tests/main/interfaces-hooks/task.yaml new file mode 100644 index 00000000..2eab470c --- /dev/null +++ b/tests/main/interfaces-hooks/task.yaml @@ -0,0 +1,17 @@ +summary: Check that `snap connect` runs interface hook + +prepare: | + echo "Build test hooks package" + snap pack $TESTSLIB/snaps/basic-iface-hooks-consumer + snap pack $TESTSLIB/snaps/basic-iface-hooks-producer + snap install --dangerous basic-iface-hooks-consumer_1.0_all.snap + snap install --dangerous basic-iface-hooks-producer_1.0_all.snap + +restore: | + rm -f basic-iface-hooks-consumer_1.0_all.snap + rm -f basic-iface-hooks-producer_1.0_all.snap + +execute: | + echo "Test that snap connect with plug and slot hooks succeeds" + + snap connect basic-iface-hooks-consumer:foo basic-iface-hooks-producer:bar diff --git a/tests/main/interfaces-iio/task.yaml b/tests/main/interfaces-iio/task.yaml new file mode 100644 index 00000000..a7a6dc5d --- /dev/null +++ b/tests/main/interfaces-iio/task.yaml @@ -0,0 +1,48 @@ +summary: Check that IIO device nodes are accessible through an interface + +details: | + This test makes sure that a snap using the IIO interface can access + devices nodes exposed by a slot properly. + + It modifies the core snap to provide a iio slot. The actual iio device + node is served as a plain file with static text content. The test expects + that, after a snap declared a iio plug is installed and connected, it can + access the node and read/write its content. + +systems: [ubuntu-core-16-64] + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + # Mock IIO device node and give it some content we can verify + # the test snap can read. + echo "iio-0" > /dev/iio:device0 + + echo "Given a snap declaring a plug on iio is installed" + . $TESTSLIB/snaps.sh + install_local iio-consumer + + echo "And the iio plug is connected" + snap connect iio-consumer:iio core:iio0 + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + rm -f /dev/iio:device0 + +execute: | + . $TESTSLIB/dirs.sh + + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + test "`$SNAP_MOUNT_DIR/bin/iio-consumer.read`" = "iio-0" + + $SNAP_MOUNT_DIR/bin/iio-consumer.write "hello" + test "`$SNAP_MOUNT_DIR/bin/iio-consumer.read`" = "hello" diff --git a/tests/main/interfaces-kernel-module-control/task.yaml b/tests/main/interfaces-kernel-module-control/task.yaml new file mode 100644 index 00000000..f8dcdeb8 --- /dev/null +++ b/tests/main/interfaces-kernel-module-control/task.yaml @@ -0,0 +1,126 @@ +summary: Ensure that the kernel-module-control interface works. + +systems: [-fedora-*, -opensuse-*] + +environment: + MODULE: minix + MODULE_PATH: /lib/modules/$(uname -r)/kernel/fs/$MODULE/$MODULE.ko + +details: | + The kernel-module-control interface allows insertion, removal and querying + of modules. + + A snap which defines a kernel-module-control plug must be shown in the + interfaces list. The plug must not be autoconnected on install and, as + usual, must be able to be reconnected. + + A snap declaring a plug on this interface must be able to list the modules + loaded, insert and remove a module. For the test we use the $MODULE module. + +prepare: | + echo "Given a snap declaring a plug on the kernel-module-control interface is installed" + snap install --edge test-snapd-kernel-module-consumer + . $TESTSLIB/snaps.sh + install_generic_consumer kernel-module-control + +restore: | + rm -f *.error + if lsmod | MATCH $MODULE && ! -f module_present; then + rmmod $MODULE + elif [ -f module_present ]; then + insmod $MODULE_PATH + fi + rm -f module_present + +debug: | + lsmod + ls -R /lib/modules/$(uname -r)/kernel/fs + +execute: | + CONNECTED_PATTERN=":kernel-module-control +.*test-snapd-kernel-module-consumer" + DISCONNECTED_PATTERN="\- +test-snapd-kernel-module-consumer:kernel-module-control" + + echo "The plug is disconnected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "===================================" + + echo "When the plug is connected" + snap connect test-snapd-kernel-module-consumer:kernel-module-control + snap connect generic-consumer:kernel-module-control + snap interfaces | MATCH "$CONNECTED_PATTERN" + + + echo "Then the snap is able to list the existing modules" + [ $(su -l -c "test-snapd-kernel-module-consumer.lsmod" test | wc -l) -gt 2 ] + + echo "And the snap is able to insert a module" + if lsmod | MATCH $MODULE; then + touch module_present + rmmod minix + fi + lsmod | MATCH -v $MODULE + test-snapd-kernel-module-consumer.insmod $MODULE_PATH + + echo "And the snap is able to read /sys/module" + generic-consumer.cmd ls /sys/module | MATCH $MODULE + + echo "And the snap is not able to write to /sys/module" + if su -l -c "generic-consumer.cmd touch /sys/module/test 2>${PWD}/touch.error" test; then + echo "Expected permission error writing to /sys/module" + exit 1 + fi + cat touch.error | MATCH "Permission denied" + + echo "And the snap is able to remove a module" + test-snapd-kernel-module-consumer.rmmod $MODULE + lsmod | MATCH -v $MODULE + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-kernel-module-consumer:kernel-module-control + snap disconnect generic-consumer:kernel-module-control + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to list modules" + if su -l -c "test-snapd-kernel-module-consumer.lsmod 2>${PWD}/list.error" test; then + echo "Expected permission error listing modules with disconnected plug" + exit 1 + fi + cat list.error | MATCH "Permission denied" + + echo "And the snap is not able to insert a module" + if test-snapd-kernel-module-consumer.insmod $MODULE_PATH; then + echo "Expected permission error inserting module with disconnected plug" + exit 1 + fi + + echo "And the snap is not able to remove a module" + # first we need to insert the module + lsmod | MATCH -v $MODULE + insmod $MODULE_PATH + lsmod | MATCH $MODULE + if test-snapd-kernel-module-consumer.rmmod $MODULE 2>${PWD}/remove.error; then + echo "Expected permission error removing module with disconnected plug" + exit 1 + fi + cat remove.error | MATCH "Permission denied" + + echo "And the snap is not able to read /sys/module" + if su -l -c "generic-consumer.cmd ls /sys/module 2>${PWD}/read.error" test; then + echo "Expected permission error reading /sys/module with disconnected plug" + exit 1 + fi + cat read.error | MATCH "Permission denied" + + echo "And the snap is not able to write to /sys/module" + if su -l -c "generic-consumer.cmd touch /sys/module/test 2>${PWD}/touch.error" test; then + echo "Expected permission error writing to /sys/module" + exit 1 + fi + cat touch.error | MATCH "Permission denied" diff --git a/tests/main/interfaces-libvirt/task.yaml b/tests/main/interfaces-libvirt/task.yaml new file mode 100644 index 00000000..4368fa12 --- /dev/null +++ b/tests/main/interfaces-libvirt/task.yaml @@ -0,0 +1,78 @@ +summary: Ensure that the libvirt interface works. + +systems: [ubuntu-16.04-64] + +details: | + The libvirt interface allows a snap to access the libvirtd socket in order to manage + libvirt domains and other resources. + + A snap which defines a libvirt plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to create and destroy a domain. + The test uses a snap that carries a unikernel built to be run on top of qemu, boot and + respond to ping. Once the domain is created, the test checks connectivity to the unikernel. + +prepare: | + # Given test user is added to the libvirtd group + adduser test libvirtd + + echo "And libvirt is configured to manage /dev/net/tun" + systemctl stop libvirtd.service || true + echo 'cgroup_device_acl = ["/dev/net/tun", "/dev/random", "/dev/urandom"]' | tee -a /etc/libvirt/qemu.conf + + echo "And the required services up" + systemctl start libvirtd.service + systemctl start virtlogd.socket + + echo "And a snap declaring a plug on the libvirt interface is installed" + snap install --edge test-snapd-libvirt-consumer + + echo "And the required tap interface is in place" + ip tuntap add tap100 mode tap + ip addr add 10.0.0.1/24 dev tap100 + ip link set dev tap100 up + +restore: | + ip link delete tap100 + + # remove test user from the libvirtd group + deluser test libvirtd + +execute: | + CONNECTED_PATTERN=":libvirt +test-snapd-libvirt-consumer" + DISCONNECTED_PATTERN="\- +test-snapd-libvirt-consumer:libvirt" + + echo "The plug is not connected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "===================================" + + echo "When the plug is connected" + snap connect test-snapd-libvirt-consumer:libvirt + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to create the unikernel domain" + su -l -c "test-snapd-libvirt-consumer.machine-up" test + virsh list | MATCH ping-unikernel + + echo "And the unikernel is accesible" + ping -c 1 -q -W 1 10.0.0.2 + + echo "And the snap is able to destroy the unikernel domain" + su -l -c "test-snapd-libvirt-consumer.machine-down" test + virsh list | MATCH -v ping-unikernel + + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-libvirt-consumer:libvirt + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to create a domain" + if su -l -c "test-snapd-libvirt-consumer.machine-up 2>${PWD}/creation.error" test; then + echo "Expected permission error accessing libvirtd socket with disconnected plug" + exit 1 + fi + cat creation.error | MATCH "Failed to connect socket to '/var/run/libvirt/libvirt-sock': Permission denied" diff --git a/tests/main/interfaces-locale-control/task.yaml b/tests/main/interfaces-locale-control/task.yaml new file mode 100644 index 00000000..ae8b928a --- /dev/null +++ b/tests/main/interfaces-locale-control/task.yaml @@ -0,0 +1,106 @@ +summary: Ensure that the locale-control interface works. + +systems: [-fedora-*, -opensuse-*] + +summary: | + The locale-control interface allows a snap to access the locale configuration. + + A snap which defines the locale-control plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to access the /etc/default/locale + file both for reading and writing. This path doesn't exist on the excluded distributions. + +prepare: | + if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then + if snap interfaces | MATCH locale-control; then + echo "locale-control should be only available on core" + exit 1 + else + exit 0 + fi + fi + + echo "Given a snap declaring a plug on the locale-control interface is installed" + snap pack $TESTSLIB/snaps/locale-control-consumer + snap install --dangerous locale-control-consumer_1.0_all.snap + mv /etc/default/locale locale.back + cat > /etc/default/locale <${PWD}/locale-read.error" test; then + echo "Expected permission error accessing locale configuration with disconnected plug" + exit 1 + fi + grep -q "Permission denied" locale-read.error + + echo "===================================" + fi + + echo "When the plug is connected" + snap connect locale-control-consumer:locale-control + + echo "Then the snap is able to write the locale configuration" + locale-control-consumer.set LANG mylang + grep -q "LANG=\"mylang\"" /etc/default/locale + + if [ "$(snap debug confinement)" = strict ] ; then + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect locale-control-consumer:locale-control + + echo "Then the snap is not able to read the locale configuration" + if locale-control-consumer.set LANG mysecondlang 2>${PWD}/locale-write.error; then + echo "Expected permission error accessing locale configuration with disconnected plug" + exit 1 + fi + grep -q "Permission denied" locale-write.error + fi diff --git a/tests/main/interfaces-log-observe/task.yaml b/tests/main/interfaces-log-observe/task.yaml new file mode 100644 index 00000000..6b768fde --- /dev/null +++ b/tests/main/interfaces-log-observe/task.yaml @@ -0,0 +1,68 @@ +summary: Check that the log-observe interface works. + +details: | + The log-observe interface allows a snap to read system logs and set kernel + log rate-limiting. + + A snap which defines the log-observe plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + +environment: + SNAP_NAME: log-observe-consumer + SNAP_FILE: "${SNAP_NAME}_1.0_all.snap" + PLUG: log-observe + +prepare: | + echo "Given a snap declaring the $PLUG plug is installed" + snap pack $TESTSLIB/snaps/$SNAP_NAME + snap install --dangerous $SNAP_FILE + +restore: | + rm -f $SNAP_FILE + +execute: | + CONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + :$PLUG +$SNAP_NAME" + DISCONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + - +$SNAP_NAME:$PLUG" + + echo "Then the snap is not listed as connected" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is connected" + snap connect $SNAP_NAME:$PLUG + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the plug can be disconnected again" + snap disconnect $SNAP_NAME:$PLUG + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is connected" + snap connect $SNAP_NAME:$PLUG + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to access the system logs" + log-observe-consumer | grep -Pqz "ok\n" + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect $SNAP_NAME:$PLUG + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then snap can't access the system logs" + if log-observe-consumer; then + echo "System log shouldn't be accessible" + exit 1 + fi diff --git a/tests/main/interfaces-many/task.yaml b/tests/main/interfaces-many/task.yaml new file mode 100644 index 00000000..3562d48b --- /dev/null +++ b/tests/main/interfaces-many/task.yaml @@ -0,0 +1,108 @@ +summary: Ensure that commands run when their interfaces are connected + +details: | + Install a test snap that plugs as many interfaces as is possible and + verify the command can run (ie, don't test the interface functionality + itself). This will help catch things like AppArmor policy syntax errors, + seccomp policy parsing, udev querying bugs, etc. + +# Ideally we would run this everywhere, but on systems with full security +# support, it takes a while, which leads to travis timeouts. Limit to: +# - Ubuntu Core 16 amd64 +# - Ubuntu classic 14.04 i386 VM +# - Ubuntu classic 16.04 amd64 VM +# - Ubuntu classic 18.04 amd64 VM +# - All Ubuntu autopkgtests +# - Debian sid amd64 VM +# - Debian 9 amd64 VM +# - TODO: All Fedora systems (for classic-only; unrelated error elsewhere) +systems: [ubuntu-core-16-64, ubuntu-14.04-32, ubuntu-16.04-64, ubuntu-18.04-64, ubuntu-*-amd64, ubuntu-*-armhf, ubuntu-*-arm64, ubuntu-*-i386, ubuntu-*-ppc64el, debian-*] + +execute: | + . $TESTSLIB/dirs.sh + + PROVIDER_SNAP="test-snapd-policy-app-provider-classic" + # quick test to see if on a core system or not + if snap list | MATCH gadget ; then + PROVIDER_SNAP="test-snapd-policy-app-provider-core" + fi + + echo "Given a snap is installed" + . $TESTSLIB/snaps.sh + install_local "$PROVIDER_SNAP" + + CONSUMER_SNAP="test-snapd-policy-app-consumer" + install_local "$CONSUMER_SNAP" + + echo "For each snap-provided slot from $PROVIDER_SNAP" + for slotcmd in "$SNAP_MOUNT_DIR"/bin/"$PROVIDER_SNAP".* ; do + slotcmd_bn=$(basename "$slotcmd") + slot_iface=$(echo "$slotcmd_bn" | tr '.' ':') + + plugcmd=$(echo $slotcmd | sed "s/$PROVIDER_SNAP/$CONSUMER_SNAP/") + plugcmd_bn=$(basename "$plugcmd") + plug_iface=$(echo "$plugcmd_bn" | tr '.' ':') + + CONNECTED_PATTERN="$slot_iface +$CONSUMER_SNAP" + DISCONNECTED_PATTERN="$slot_iface +-" + + echo "When slot $slot_iface is connected" + if snap interfaces | MATCH "$DISCONNECTED_PATTERN" ; then + snap connect "$plug_iface" "$slot_iface" + else + echo "$plug_iface already connected" + fi + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then $slotcmd_bn should succeed" + "$slotcmd" | MATCH PASS + + echo "Then $plugcmd_bn should succeed" + "$plugcmd" | MATCH PASS + done + + echo "For each core-provided slot" + for plugcmd in "$SNAP_MOUNT_DIR"/bin/"$CONSUMER_SNAP".* ; do + plugcmd_bn=$(basename "$plugcmd") + plug_iface=$(echo "$plugcmd_bn" | tr '.' ':') + slot_iface=$(echo "$plug_iface" | sed "s/$CONSUMER_SNAP//") + + # we test browser-support two different ways, so account for that + if [ "$plug_iface" = "$CONSUMER_SNAP:browser-sandbox" ]; then + slot_iface=":browser-support" + fi + + CONNECTED_PATTERN="$slot_iface +$CONSUMER_SNAP" + DISCONNECTED_PATTERN="$slot_iface +-" + + # The core-support plug is connected by the core snap, so account for + # that + if [ "$plug_iface" = "$CONSUMER_SNAP:core-support" ]; then + CONNECTED_PATTERN="$slot_iface +core:core-support-plug,$CONSUMER_SNAP" + DISCONNECTED_PATTERN="$slot_iface +core:core-support-plug$" + fi + + # Skip any interfaces that core doesn't ship + if ! snap interfaces | MATCH "$slot_iface +" ; then + echo "$slot_iface not present, skipping" + continue + fi + + echo "When slot $slot_iface is connected" + if snap interfaces | MATCH "$DISCONNECTED_PATTERN" ; then + if [ "$slot_iface" = ":broadcom-asic-control" ] || [ "$slot_iface" = ":firewall-control" ] || [ "$slot_iface" = ":kubernetes-support" ] || [ "$slot_iface" = ":openvswitch-support" ] || [ "$slot_iface" = ":ppp" ]; then + # TODO: when the kmod backend no longer fails on missing + # modules, we can remove this + snap connect "$plug_iface" "$slot_iface" || true + else + snap connect "$plug_iface" "$slot_iface" + fi + fi + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then $slotcmd_bn should succeed" + "$slotcmd" | MATCH PASS + + echo "Then $plugcmd_bn should succeed" + "$plugcmd" | MATCH PASS + done diff --git a/tests/main/interfaces-mount-observe/task.yaml b/tests/main/interfaces-mount-observe/task.yaml new file mode 100644 index 00000000..e421ec76 --- /dev/null +++ b/tests/main/interfaces-mount-observe/task.yaml @@ -0,0 +1,67 @@ +summary: Ensures that the mount-observe interface works + +details: | + A snap declaring the mount-observe plug is defined, its command + just read the /proc//mounts file. + + The test itself checks for the lack of autoconnect and then tries + to execute the snap command with the plug connected (it must succeed) + and disconnected (it must fail). + + The test also checks that a new mount created after the snap is installed + is also shown when the plug is connected. + +prepare: | + echo "Given a snap declaring a plug on the mount-observe interface is installed" + snap pack $TESTSLIB/snaps/mount-observe-consumer + snap install --dangerous mount-observe-consumer_1.0_all.snap + +restore: | + rm -f mount-observe-consumer_1.0_all.snap + +execute: | + . $TESTSLIB/dirs.sh + + CONNECTED_PATTERN=":mount-observe +mount-observe-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +mount-observe-consumer:mount-observe" + + echo "Then the plug is shown as disconnected" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "=========================================" + + echo "When the plug is connected" + snap connect mount-observe-consumer:mount-observe + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the mount info is reachable" + expected="$SNAP_MOUNT_DIR/mount-observe-consumer" + su -l -c "mount-observe-consumer" test | grep -Pq "$expected" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "=========================================" + + echo "When the plug is disconnected" + snap disconnect mount-observe-consumer:mount-observe + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the mount info is not reachable" + if su -l -c "mount-observe-consumer" test; then + echo "Expected error accessing mount info with disconnected plug" + exit 1 + fi + fi + + echo "=========================================" + + echo "When the plug is connected" + snap connect mount-observe-consumer:mount-observe + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "And a new mount is created" + . "$TESTSLIB/snaps.sh" + install_local test-snapd-tools + + echo "Then the new mount info is reachable" + expected="$SNAP_MOUNT_DIR/test-snapd-tools" + su -l -c "mount-observe-consumer" test | grep -Pq "$expected" diff --git a/tests/main/interfaces-network-bind/task.yaml b/tests/main/interfaces-network-bind/task.yaml new file mode 100644 index 00000000..3061cf5f --- /dev/null +++ b/tests/main/interfaces-network-bind/task.yaml @@ -0,0 +1,77 @@ +summary: Ensure that the network-bind interface works + +details: | + The network-bind interface allows a daemon to access the network as a server. + + A snap which defines the network-bind plug must be shown in the interfaces list. + The plug must be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be accessible by a network client. + +environment: + SNAP_NAME: network-bind-consumer + SNAP_FILE: ${SNAP_NAME}_1.0_all.snap + PORT: 8081 + REQUEST_FILE: ./request.txt + +prepare: | + echo "Given a snap declaring the network-bind plug is installed" + snap pack $TESTSLIB/snaps/$SNAP_NAME + snap install --dangerous $SNAP_FILE + + echo "Given the snap's service is listening" + while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done + + echo "Given we store a basic HTTP request" + cat > $REQUEST_FILE </dev/null || true + ip netns delete canary 2>/dev/null || true diff --git a/tests/main/interfaces-network-control-tuntap/task.yaml b/tests/main/interfaces-network-control-tuntap/task.yaml new file mode 100644 index 00000000..947e8a3b --- /dev/null +++ b/tests/main/interfaces-network-control-tuntap/task.yaml @@ -0,0 +1,43 @@ +summary: Ensure that the network-control interface works with TUN/TAP. + +details: | + The network-control interface allows a snap to configure networking of + TUN/TAP devices. + + A snap declaring a plug on this interface must be able to allocate TUN/TAP + virtual network devices. + + https://github.com/torvalds/linux/blob/master/Documentation/networking/tuntap.txt + +# This test is randomly failing when running with the full suite. +# It may be a race or a sequence problem with an earlier test. +manual: true + +environment: + DEV/tun: tun255 + DEV/tap: tap255 + +execute: | + echo "Given a snap declaring a plug on the network-control interface is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-tuntap + + . "$TESTSLIB/network.sh" + + CONNECTED_PATTERN=":network-control +test-snapd-tuntap" + DISCONNECTED_PATTERN="^- +test-snapd-tuntap:network-control$" + + echo "Then the plug disconnected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "And cannot allocate the $DEV device" + test-snapd-tuntap.tuntap $DEV 2>&1 | MATCH "Permission denied" + fi + + echo "When the plug is connected" + snap connect test-snapd-tuntap:network-control + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap command can allocate the $DEV device" + test-snapd-tuntap.tuntap $DEV | MATCH "PASS" diff --git a/tests/main/interfaces-network-control/task.yaml b/tests/main/interfaces-network-control/task.yaml new file mode 100644 index 00000000..0729c55e --- /dev/null +++ b/tests/main/interfaces-network-control/task.yaml @@ -0,0 +1,159 @@ +summary: Ensure that the network-control interface works. + +systems: [-fedora-*, -opensuse-*] + +details: | + The network-control interface allows a snap to configure networking. + + A snap which defines the network-control plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to modify the network configuration + and ask for its status, the test sets up a network service, gets information about it (read + capability) and creates an arp entry (write capability). + +environment: + PORT: 8081 + SERVICE_FILE: "./service.sh" + SERVICE_NAME: "test-service" + ARP_ENTRY_ADDR: "30.30.30.30" + +prepare: | + . "$TESTSLIB/systemd.sh" + + echo "Given a snap declaring a plug on the network-control interface is installed" + snap pack $TESTSLIB/snaps/network-control-consumer + snap install --dangerous network-control-consumer_1.0_all.snap + + echo "And a network service is up" + printf "#!/bin/sh -e\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT; done" > $SERVICE_FILE + chmod a+x $SERVICE_FILE + systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)" + + while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done + +restore: | + . "$TESTSLIB/systemd.sh" + . "$TESTSLIB/network.sh" + + systemd_stop_and_destroy_unit $SERVICE_NAME + rm -f network-control-consumer_1.0_all.snap *.output $SERVICE_FILE + arp -d $ARP_ENTRY_ADDR -i $(get_default_iface) || true + + ip netns delete test-ns || true + ip link delete veth0 || true + +execute: | + . "$TESTSLIB/network.sh" + + CONNECTED_PATTERN=":network-control +network-control-consumer" + DISCONNECTED_PATTERN="^- +network-control-consumer:network-control$" + INTERFACE=$(get_default_iface) + + echo "Then the plug disconnected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "====================================" + + echo "When the plug is connected" + snap connect network-control-consumer:network-control + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap command can query network status information" + network-control-consumer.cmd netstat -lnt | MATCH "0.0.0.0:$PORT.*?LISTEN" + + echo "====================================" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "When the plug is disconnected" + snap disconnect network-control-consumer:network-control + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap command can not query network status information" + if network-control-consumer.cmd netstat -lnt 2>net-query.output; then + echo "Expected error caling command with disconnected plug" + exit 1 + fi + cat net-query.output | MATCH "Permission denied" + + echo "====================================" + fi + + echo "When the plug is connected" + snap connect network-control-consumer:network-control + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap command can modify the network configuration" + network-control-consumer.cmd arp -s "$ARP_ENTRY_ADDR" aa:aa:aa:aa:aa:aa -i "$INTERFACE" + expected="(?s)br0.*?state UP.*?bridge.*?foo@bar.*?veth.*?bar@foo.*?veth" + arp | MATCH "$ARP_ENTRY_ADDR.*?ether.*?CM" + + echo "====================================" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "When the plug is disconnected" + snap disconnect network-control-consumer:network-control + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap command can not modify the network configuration" + if network-control-consumer.cmd arp -s "$ARP_ENTRY_ADDR" aa:aa:aa:aa:aa:aa -i "$INTERFACE" 2>net-command.output; then + echo "Expected error calling command with disconnected plug" + exit 1 + fi + cat net-command.output | MATCH "Permission denied" + + echo "====================================" + fi + + echo "When the plug is connected" + snap connect network-control-consumer:network-control + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "A network namespace can be created" + network-control-consumer.cmd ip netns add test-ns + ip netns list | MATCH test-ns + + echo "And a veth interface can be added to the namespace" + + ip link add veth0 type veth peer name veth1 + ip link list | MATCH "veth0.*veth1" + + network-control-consumer.cmd ip link set veth1 netns test-ns + + ip link list | MATCH "veth0" + ip link list | MATCH -v "veth1" + + echo "And a command can be executed in the context of the namespace" + network-control-consumer.cmd ip netns exec test-ns ip link list | MATCH "veth1" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "====================================" + + echo "When the plug is disconnected" + snap disconnect network-control-consumer:network-control + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "The snap is not able to create a network namespace" + if network-control-consumer.cmd ip netns add test-ns-2 2>ns-create.output; then + echo "Expected error calling ns create command with disconnected plug" + fi + cat ns-create.output | MATCH "Permission denied" + + echo "And the snap can't add a veth interface to an existing namespace" + # first, move veth1 back to the root namespace + ip netns exec test-ns ip link set veth1 netns 1 + if network-control-consumer.cmd ip link set veth1 netns test-ns 2>ns-move.output; then + echo "Expected error trying to move veth to network namespace with disconnected plug" + exit 1 + fi + cat ns-move.output | MATCH "Permission denied" + + + echo "And the snap can't execute a command in the context of the namespace" + if network-control-consumer.cmd ip netns exec test-ns ip link list 2>ns-exec.output; then + echo "Expected error trying to execute command in a network namespace context with disconnected plug" + exit 1 + fi + cat ns-exec.output | MATCH "Permission denied" + fi diff --git a/tests/main/interfaces-network-manager/task.yaml b/tests/main/interfaces-network-manager/task.yaml new file mode 100644 index 00000000..d8b52c75 --- /dev/null +++ b/tests/main/interfaces-network-manager/task.yaml @@ -0,0 +1,56 @@ +summary: Ensure that the network-manager interface works + +description: | + The network-manager interface gives privileged access to configure and + observe networking. + The test uses a snap which plugs the network manager interface. Then it is + validated that the snap autoconects and can create a new connection. + Connection against network devices cannot be validated on a virtual machine + due to network-manager being configured not to managed them. + +# run only against the amd64 VM, we cannot run this on arm/arm64 +# boards because the (wifi) network is already managed by netplan +# there and when n-m gets installed/removed it will hang when +# trying to deconfigure the wifi network which is already owned. +systems: [ubuntu-core-16-64, ubuntu-core-16-32] + +execute: | + CONNECTED_PATTERN="^network-manager:service +network-manager:nmcli" + DISCONNECTED_PATTERN="^network-manager:service +-$" + + echo "Give a network-manager snap is installed" + snap install network-manager + + # using wait_for_service is not enough, systemd considers the service + # active even when it is not (yet) listening to dbus + for i in $(seq 300); do + if network-manager.nmcli general; then + break + fi + sleep 1 + done + + echo "Then the plug is connected by default" + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "And allows to add a new connection" + conn_name=nmtest + network-manager.nmcli con add type ethernet con-name $conn_name ifname eth0 | MATCH "successfully added" + network-manager.nmcli c | MATCH "^$conn_name .+ethernet +" + + echo "And allows to remove a connection" + network-manager.nmcli connection delete id $conn_name | MATCH "successfully deleted" + + echo "And allows to show devices information" + network-manager.nmcli d show + + echo "When the plug is disconnected" + snap disconnect network-manager:nmcli + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the consumer is not able to access the provided methods" + if network-manager.nmcli general 2>${PWD}/call.error; then + echo "Expected permission error calling nmcli method with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < call.error diff --git a/tests/main/interfaces-network-observe/task.yaml b/tests/main/interfaces-network-observe/task.yaml new file mode 100644 index 00000000..fc5ad03b --- /dev/null +++ b/tests/main/interfaces-network-observe/task.yaml @@ -0,0 +1,68 @@ +summary: Ensure that the network-observe interface works + +systems: [-fedora-*, -opensuse-*] + +details: | + The network-observe interface allows a snap to query the network status information. + + A snap which defines the network-observe plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to access read the network status, + the test sets up a network service to establish a known state in the network to be queried. + +environment: + PORT: 8081 + SERVICE_FILE: "./service.sh" + SERVICE_NAME: "test-service" + +prepare: | + . "$TESTSLIB/systemd.sh" + + echo "Given a snap declaring a plug on the network-observe interface is installed" + snap pack $TESTSLIB/snaps/network-observe-consumer + snap install --dangerous network-observe-consumer_1.0_all.snap + + echo "And a network service is up" + printf "#!/bin/sh -e\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT; done" > $SERVICE_FILE + chmod a+x $SERVICE_FILE + systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)" + + while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done + +restore: | + . "$TESTSLIB/systemd.sh" + + systemd_stop_and_destroy_unit $SERVICE_NAME + rm -f network-observe-consumer_1.0_all.snap net-query.output $SERVICE_FILE + +execute: | + CONNECTED_PATTERN=":network-observe +network-observe-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +network-observe-consumer:network-observe" + + echo "Then the plug disconnected by default" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "====================================" + + echo "When the plug is connected" + snap connect network-observe-consumer:network-observe + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap command can query network status information" + network-observe-consumer | grep -P "0.0.0.0:$PORT.*?LISTEN" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "====================================" + + echo "When the plug is disconnected" + snap disconnect network-observe-consumer:network-observe + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the snap command can not query network status information" + if network-observe-consumer 2>net-query.output; then + echo "Expected error caling command with disconnected plug" + fi + cat net-query.output | grep -Pq "Permission denied" + fi diff --git a/tests/main/interfaces-network-setup-control/task.yaml b/tests/main/interfaces-network-setup-control/task.yaml new file mode 100644 index 00000000..f478e936 --- /dev/null +++ b/tests/main/interfaces-network-setup-control/task.yaml @@ -0,0 +1,60 @@ +summary: Ensure that the desktop interface works. + +details: | + The network setup control interface allows to access the different netplan + configuration files. + + The test uses the test-snapd-check-fs-access snap to checks files and dirs + are accessible through the interface. + +systems: [-ubuntu-core-*] + +prepare: | + echo "Given the test-snapd-check-fs-access snap is installed" + cp $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml.orig + sed 's/\[\]/\[network-setup-control\]/g' -i $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml + snap try $TESTSLIB/snaps/test-snapd-check-fs-access + +restore: | + cp $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml.orig $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml + +execute: | + CONNECTED_PATTERN=":network-setup-control +test-snapd-check-fs-access" + DISCONNECTED_PATTERN="\- +test-snapd-check-fs-access:network-setup-control" + + dirs="/etc/netplan /etc/network" + + echo "The plug is not connected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "When the interface is connected" + snap connect test-snapd-check-fs-access:network-setup-control + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to write in the network and netplan directories" + for dir in $dirs; do + if [ -d $dir ]; then + test-snapd-check-fs-access.write-dir $dir + fi + done + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-check-fs-access:network-setup-control + + echo "Then the snap is not able to access the networking configuration dirs" + for dir in $dirs; do + if [ -d $dir ]; then + if test-snapd-check-fs-access.write-dir $dir 2>${PWD}/call.error; then + echo "Expected permission error calling desktop with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < call.error + fi + done + + echo "Then the interface can be connected again" + snap connect test-snapd-check-fs-access:network-setup-control diff --git a/tests/main/interfaces-network-setup-observe/task.yaml b/tests/main/interfaces-network-setup-observe/task.yaml new file mode 100644 index 00000000..39ffc585 --- /dev/null +++ b/tests/main/interfaces-network-setup-observe/task.yaml @@ -0,0 +1,58 @@ +summary: Ensure that the desktop interface works. + +details: | + The network setup observe interface allows to access the different netplan + configuration files. + + The test uses the test-snapd-check-fs-access snap to checks files and dirs + are accessible through the interface. + +prepare: | + echo "Given the test-snapd-check-fs-access snap is installed" + cp $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml.orig + sed 's/\[\]/\[network-setup-observe\]/g' -i $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml + snap try $TESTSLIB/snaps/test-snapd-check-fs-access + +restore: | + cp $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml.orig $TESTSLIB/snaps/test-snapd-check-fs-access/meta/snap.yaml + +execute: | + CONNECTED_PATTERN=":network-setup-observe +test-snapd-check-fs-access" + DISCONNECTED_PATTERN="\- +test-snapd-check-fs-access:network-setup-observe" + + dirs="/etc/netplan /etc/network" + + echo "The plug is not connected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "When the interface is connected" + snap connect test-snapd-check-fs-access:network-setup-observe + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to read the network and netplan directories" + for dir in $dirs; do + if [ -d $dir ]; then + test-snapd-check-fs-access.read-dir $dir + fi + done + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-check-fs-access:network-setup-observe + + echo "Then the snap is not able to read the network and netplan directories" + for dir in $dirs; do + if [ -d $dir ]; then + if test-snapd-check-fs-access.read-dir $dir 2>${PWD}/call.error; then + echo "Expected permission error calling desktop with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < call.error + fi + done + + echo "Then the interface can be connected again" + snap connect test-snapd-check-fs-access:network-setup-observe diff --git a/tests/main/interfaces-network/task.yaml b/tests/main/interfaces-network/task.yaml new file mode 100644 index 00000000..2219b496 --- /dev/null +++ b/tests/main/interfaces-network/task.yaml @@ -0,0 +1,79 @@ +summary: Ensure network interface works. + +systems: [-fedora-*, -opensuse-*] + +details: | + The network interface allows a snap to access the network as a client. + + A snap which defines the network plug must be shown in the interfaces list. + The plug must be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to access network services. + +environment: + SNAP_NAME: network-consumer + SNAP_FILE: ${SNAP_NAME}_1.0_all.snap + PORT: 8081 + SERVICE_FILE: "./service.sh" + SERVICE_NAME: "test-service" + +prepare: | + . $TESTSLIB/systemd.sh + echo "Given a snap declaring the network plug is installed" + snap pack $TESTSLIB/snaps/$SNAP_NAME + snap install --dangerous $SNAP_FILE + + echo "And a service is listening" + printf "#!/bin/sh -e\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT -q 1; done" > $SERVICE_FILE + chmod a+x $SERVICE_FILE + systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)" + + while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done + +restore: | + . $TESTSLIB/systemd.sh + systemd_stop_and_destroy_unit $SERVICE_NAME + rm -f $SNAP_FILE $SERVICE_FILE + +execute: | + CONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + :network +$SNAP_NAME" + DISCONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + - +$SNAP_NAME:network" + + echo "Then the snap is listed as connected" + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect $SNAP_NAME:network + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the plug can be connected again" + snap connect $SNAP_NAME:network + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is connected" + snap connect $SNAP_NAME:network + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to access a network service" + network-consumer http://127.0.0.1:$PORT | grep -Pqz "ok\n" + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect $SNAP_NAME:network + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then snap can't access a network service" + if network-consumer http://127.0.0.1:$PORT; then + echo "Network shouldn't be accessible" + exit 1 + fi diff --git a/tests/main/interfaces-openvswitch/task.yaml b/tests/main/interfaces-openvswitch/task.yaml new file mode 100644 index 00000000..44e5d32f --- /dev/null +++ b/tests/main/interfaces-openvswitch/task.yaml @@ -0,0 +1,118 @@ +summary: Ensure that the openvswitch interface works. + +systems: + - -ubuntu-core-* + +details: | + The openvswitch interface allows to task to the openvswitch socket (rw mode). + + A snap which defines a openvswitch plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to do all the operations that + are carried through the socket, in this test we exercise bridge and port creation, + list and deletion. + +# marking as manual due to errors during prepare +manual: true + +prepare: | + . "$TESTSLIB/pkgdb.sh" + + echo "Given openvswitch is installed" + distro_install_package --no-install-recommends openvswitch-switch + + # Ensure the openvswitch service is started which isn't the case by + # default on all distributions + systemctl enable --now openvswitch + + echo "And a snap declaring a plug on the openvswitch interface is installed" + snap install --edge test-snapd-openvswitch-consumer + + echo "And a tap interface is defined" + ip tuntap add tap1 mode tap + +restore: | + . "$TESTSLIB/pkgdb.sh" + + ovs-vsctl del-port br0 tap1 || true + ovs-vsctl del-br br0 || true + + distro_purge_package openvswitch-switch + distro_auto_remove_packages + + rm -f *.error + + ip link delete tap1 || true + +execute: | + CONNECTED_PATTERN=":openvswitch +test-snapd-openvswitch-consumer" + DISCONNECTED_PATTERN="\- +test-snapd-openvswitch-consumer:openvswitch" + + echo "The plug is not connected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "===================================" + + echo "When the plug is connected" + snap connect test-snapd-openvswitch-consumer:openvswitch + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to create a bridge" + test-snapd-openvswitch-consumer.ovs-vsctl add-br br0 + ovs-vsctl list-br | MATCH br0 + + echo "And the snap is able to create a port" + test-snapd-openvswitch-consumer.ovs-vsctl add-port br0 tap1 + ovs-vsctl list-ports br0 | MATCH tap1 + + echo "And the snap is able to delete a port" + test-snapd-openvswitch-consumer.ovs-vsctl del-port br0 tap1 + ovs-vsctl list-ports br0 | MATCH -v tap1 + + echo "And the snap is able to delete a bridge" + test-snapd-openvswitch-consumer.ovs-vsctl del-br br0 + ovs-vsctl list-br | MATCH -v br0 + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-openvswitch-consumer:openvswitch + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to create a bridge" + if test-snapd-openvswitch-consumer.ovs-vsctl add-br br0 2>${PWD}/bridge-creation.error; then + echo "Expected permission error accessing openvswitch socket with disconnected plug" + exit 1 + fi + cat bridge-creation.error | MATCH "database connection failed \(Permission denied\)" + + ovs-vsctl add-br br0 + + echo "And the snap is not able to create a port" + if test-snapd-openvswitch-consumer.ovs-vsctl add-port br0 tap1 2>${PWD}/port-creation.error; then + echo "Expected permission error accessing openvswitch socket with disconnected plug" + exit 1 + fi + cat port-creation.error | MATCH "database connection failed \(Permission denied\)" + + ovs-vsctl add-port br0 tap1 + + echo "And the snap is not able to delete a port" + if test-snapd-openvswitch-consumer.ovs-vsctl del-port br0 tap1 2>${PWD}/port-deletion.error; then + echo "Expected permission error accessing openvswitch socket with disconnected plug" + exit 1 + fi + cat port-deletion.error | MATCH "database connection failed \(Permission denied\)" + + echo "And the snap is not able to delete a bridge" + if test-snapd-openvswitch-consumer.ovs-vsctl del-br br0 2>${PWD}/br-creation.error; then + echo "Expected permission error accessing openvswitch socket with disconnected plug" + exit 1 + fi + cat br-creation.error | MATCH "database connection failed \(Permission denied\)" diff --git a/tests/main/interfaces-password-manager-service/task.yaml b/tests/main/interfaces-password-manager-service/task.yaml new file mode 100644 index 00000000..a18af1ba --- /dev/null +++ b/tests/main/interfaces-password-manager-service/task.yaml @@ -0,0 +1,52 @@ +summary: Ensure that the password-manager-service interface works + +# Only test on classic systems with AppArmor DBus mediation +systems: [ ubuntu-1* ] + +prepare: | + . $TESTSLIB/pkgdb.sh + echo "Ensure we have a working gnome-keyring" + snap install --edge test-snapd-password-manager-consumer + +restore: | + . $TESTSLIB/pkgdb.sh + kill $(cat dbus-launch.pid) + rm -f dbus-launch.pid + +execute: | + CONNECTED_PATTERN=":password-manager-service +test-snapd-password-manager-consumer" + DISCONNECTED_PATTERN="\- +test-snapd-password-manager-consumer:password-manager-service" + + echo "Ensure things run" + eval $(dbus-launch --sh-syntax) + eval $(printf password|gnome-keyring-daemon --login) + eval $(gnome-keyring-daemon --start) + echo "$DBUS_SESSION_BUS_PID" > dbus-launch.pid + + echo "Then it is not shown as connected" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "====================================" + + echo "When the plug is connected" + snap connect test-snapd-password-manager-consumer:password-manager-service + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap command is able use the libsecret service" + test-snapd-password-manager-consumer.secret-tool clear foo bar + + if [ "$(snap debug confinement)" = "partial" ] ; then + exit 0 + fi + + echo "====================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-password-manager-consumer:password-manager-service + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap command is not able to use the secret-tool" + if test-snapd-password-manager-consumer.secret-tool clear foo bar; then + echo "Expected error with plug disconnected" + exit 1 + fi diff --git a/tests/main/interfaces-physical-memory-observe/task.yaml b/tests/main/interfaces-physical-memory-observe/task.yaml new file mode 100644 index 00000000..92e429f5 --- /dev/null +++ b/tests/main/interfaces-physical-memory-observe/task.yaml @@ -0,0 +1,52 @@ +summary: Ensure that the physical memory observe interface works. + +details: | + The physical-memory-observe interface allows to read the physical memory. + + The test-snapd-physical-memory-observe snap checks that /dev/mem can be read + and the interface can be connected and disconnected. + +# As the ubuntu kernels are configured with CONFIG_STRICT_DEVMEM=y, this interface is +# validated just in the other supported systems. +systems: [-ubuntu-*] + +prepare: | + echo "Given the physical-memory-observe snap is installed" + snap try $TESTSLIB/snaps/test-snapd-physical-memory-observe + +restore: | + rm -f call.error + +execute: | + config="/boot/config-$(uname -r)" + if ([ -f $config ] && MATCH "CONFIG_STRICT_DEVMEM=y" < $config) || ([ -f /proc/config.gz ] && zcat /proc/config.gz | MATCH "CONFIG_STRICT_DEVMEM=y"); then + echo "Kernel option CONFIG_STRICT_DEVMEM=y, it is not possible to write in /dev/mem, exiting..." + exit 0 + fi + + CONNECTED_PATTERN=":physical-memory-observe +test-snapd-physical-memory-observe" + DISCONNECTED_PATTERN="\- +test-snapd-physical-memory-observe:physical-memory-observe" + + echo "The interface is not connected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "When the interface is connected" + snap connect test-snapd-physical-memory-observe:physical-memory-observe + + echo "Then the snap is able access to the physical memory" + test-snapd-physical-memory-observe.head + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-physical-memory-observe:physical-memory-observe + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to access the physical memory" + if test-snapd-physical-memory-observe.head 2>${PWD}/call.error; then + echo "Expected permission error accessing to physical memory with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < call.error diff --git a/tests/main/interfaces-process-control/task.yaml b/tests/main/interfaces-process-control/task.yaml new file mode 100644 index 00000000..1f1bbe8a --- /dev/null +++ b/tests/main/interfaces-process-control/task.yaml @@ -0,0 +1,61 @@ +summary: Ensure that the process-control interface works. + +summary: | + The process-control interface allows a snap to control other processes via signals + and nice. + + A snap which defines the process-control plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to kill other processes. Currently + this test does not check the priority change capability of the interface, will be + extended later. + +prepare: | + echo "Given a snap declaring a plug on the process-control interface is installed" + snap pack $TESTSLIB/snaps/process-control-consumer + snap install --dangerous process-control-consumer_1.0_all.snap + +restore: | + rm -f process-control-consumer_1.0_all.snap process-kill.error process-nice.error + +execute: | + CONNECTED_PATTERN=":process-control +process-control-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +process-control-consumer:process-control" + + echo "Then it is not connected by default" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "===================================" + + echo "When the plug is connected" + snap connect process-control-consumer:process-control + + echo "Then the snap is able to kill an existing process" + while :; do sleep 1; done & + pid=$! + ps ax | grep -Pq "^ *$pid" + process-control-consumer.signal "SIGTERM" $pid + ! ps ax | grep -Pq "^ *$pid" + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect process-control-consumer:process-control + + echo "Then the snap is not able to kill an existing process" + while :; do sleep 1; done & + pid=$! + if process-control-consumer.signal SIGTERM $pid 2>${PWD}/process-kill.error; then + echo "Expected permission error accessing killing a process with disconnected plug" + exit 1 + fi + grep -q "Permission denied" process-kill.error + ps ax | grep -Pq "^ *$pid" + kill -9 $pid + ! ps ax | grep -Pq "^ *$pid" diff --git a/tests/main/interfaces-shutdown-introspection/task.yaml b/tests/main/interfaces-shutdown-introspection/task.yaml new file mode 100644 index 00000000..0addec6f --- /dev/null +++ b/tests/main/interfaces-shutdown-introspection/task.yaml @@ -0,0 +1,49 @@ +summary: Ensures that introspection of login1 of the shutdown interface works. + +systems: + # No confinement (AppArmor, Seccomp) available on these systems + - -debian-* + # unity7 implicit classic slot needed (used to access dbus-send) not + # available on core + - -ubuntu-core-* + +details: | + A snap declaring the shutdown plug is defined, its command just calls + the Introspect method on org.freedesktop.login1. + +execute: | + echo "Given a snap declaring a plug on the shutdown interface is installed" + . $TESTSLIB/snaps.sh + install_local shutdown-introspection-consumer + + CONNECTED_PATTERN=":shutdown +shutdown-introspection-consumer" + DISCONNECTED_PATTERN="\- +shutdown-introspection-consumer:shutdown" + + echo "Then the plug is shown as disconnected" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "===========================================" + + echo "When the plug is connected" + snap connect shutdown-introspection-consumer:shutdown + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to get introspect org.freedesktop.login1" + expected="" + su -l -c "shutdown-introspection-consumer" test | MATCH "$expected" + + echo "===========================================" + + echo "When the plug is disconnected" + snap disconnect shutdown-introspection-consumer:shutdown + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = partial ]; then + exit + fi + + echo "Then the snap is not able to get system information" + if su -l -c "shutdown-introspection-consumer" test; then + echo "Expected error with plug disconnected" + exit 1 + fi diff --git a/tests/main/interfaces-snapd-control-with-manage/task.yaml b/tests/main/interfaces-snapd-control-with-manage/task.yaml new file mode 100644 index 00000000..da1543cf --- /dev/null +++ b/tests/main/interfaces-snapd-control-with-manage/task.yaml @@ -0,0 +1,94 @@ +summary: Ensure that the snapd-control "refresh-schedule" attribute works. + +environment: + BLOB_DIR: $(pwd)/fake-store-blobdir + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + echo "Ensure jq is installed" + if ! which jq; then + snap install jq + fi + + snap debug can-manage-refreshes | MATCH false + + snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" + + . $TESTSLIB/store.sh + setup_fake_store $BLOB_DIR + + . $TESTSLIB/snaps.sh + snap_path=$(make_snap test-snapd-control-consumer) + make_snap_installable $BLOB_DIR ${snap_path} + cat > snap-decl.json <<'EOF' + { + "format": "1", + "revision": "2", + "snap-name": "test-snapd-control-consumer", + "snap-id": "test-snapd-control-consumer-id", + "plugs": + { + "snapd-control": { + "allow-installation": "true", + "allow-auto-connection": "true" + } + } + } + EOF + fakestore new-snap-declaration --dir "${BLOB_DIR}" --snap-decl-json snap-decl.json + snap ack ${BLOB_DIR}/asserts/*.snap-declaration + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/store.sh + teardown_fake_store $BLOB_DIR + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + snap install test-snapd-control-consumer + snap interfaces + + echo "When the snapd-control-with-manage plug is connected" + snap connect test-snapd-control-consumer:snapd-control-with-manage + + echo "Then the system knows it can be set to managed" + snap debug can-manage-refreshes | MATCH true + + echo "Then the core refresh.schedule can be set to 'managed'" + snap set core refresh.schedule=managed + if journalctl -u snapd |grep 'cannot parse "managed"'; then + echo "refresh.schedule=managed was not rejected as it should be" + exit 1 + fi + snap refresh --time | MATCH 'schedule: managed' + + echo "Check that the snapd.refresh.service is disabled as well" + systemctl start snapd.refresh.service + if snap changes | MATCH "Refresh all snaps"; then + echo "snapd.refresh.service did refresh, this should not happen" + exit 1 + fi + + + echo "When the snapd-control-with-manage plug is disconnected" + snap disconnect test-snapd-control-consumer:snapd-control-with-manage + + echo "Then the snap refresh schedule cannot be set to managed" + if snap set core refresh.schedule=managed; then + echo "refresh.schedule=managed was not rejected as it should be" + exit 1 + fi + + echo "Ensure that last-refresh-hit happens regardless of managed setting" + cat /var/lib/snapd/state.json | jq '.data["last-refresh-hints"]' | grep $(date +%Y) diff --git a/tests/main/interfaces-snapd-control/task.yaml b/tests/main/interfaces-snapd-control/task.yaml new file mode 100644 index 00000000..a4b6d4a5 --- /dev/null +++ b/tests/main/interfaces-snapd-control/task.yaml @@ -0,0 +1,55 @@ +summary: Ensure that the snapd-control interface works. + +details: | + The snapd-control interface allows a snap to access the locale configuration. + + A snap which defines the snapd-control plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to control the snapd daemon + through the socket, the test snap used has a command to install a snap (exercising + the write capability on the socket) and a command to list the installed snaps (which + checks the read capability). A network plug must be defined and connected for the + snap to be able to talk to the socket, the snapd-control is not enough by itself. + + +prepare: | + echo "Given a snap declaring a plug on the snapd-control interface is installed" + snap install --edge test-snapd-control-consumer + +restore: | + rm -f snapd.error + +execute: | + CONNECTED_PATTERN=":snapd-control +test-snapd-control-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-control-consumer:snapd-control" + + echo "Then it is connected by default" + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "================================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-control-consumer:snapd-control + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the snap command is not able to control snapd" + if test-snapd-control-consumer.list 2>snapd.error; then + echo "Expected error with plug disconnected" + exit 1 + fi + grep -q "Permission denied" snapd.error + fi + + echo "================================================" + + echo "When the plug is connected" + snap connect test-snapd-control-consumer:snapd-control + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap command is able to control snapd" + ! test-snapd-control-consumer.list | grep -q test-snapd-tools + test-snapd-control-consumer.install test-snapd-tools + while ! test-snapd-control-consumer.list | grep -q test-snapd-tools; do sleep 1; done diff --git a/tests/main/interfaces-system-observe/task.yaml b/tests/main/interfaces-system-observe/task.yaml new file mode 100644 index 00000000..bc9d4871 --- /dev/null +++ b/tests/main/interfaces-system-observe/task.yaml @@ -0,0 +1,71 @@ +summary: Ensures that the system-observe interface works. + +details: | + A snap declaring the system-observe plug is defined, its command + just calls ps -ax. + + The test itself checks for the lack of autoconnect and then tries + to execute the snap command with the plug connected (it must succeed) + and disconnected (it must fail). + +prepare: | + echo "Given a snap declaring a plug on the system-observe interface is installed" + snap install --edge test-snapd-system-observe-consumer + + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + echo "And hostnamed is started" + systemctl start systemd-hostnamed + fi + +restore: | + rm -f *.error + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + systemctl stop systemd-hostnamed + fi + +execute: | + CONNECTED_PATTERN=":system-observe +test-snapd-system-observe-consumer" + DISCONNECTED_PATTERN="^\- +test-snapd-system-observe-consumer:system-observe" + + echo "Then the plug is shown as disconnected" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "===========================================" + + echo "When the plug is connected" + snap connect test-snapd-system-observe-consumer:system-observe + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then the snap is able to get system information" + expected="/dev/tty.*?serial" + su -l -c "test-snapd-system-observe-consumer.consumer" test | MATCH "$expected" + + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + echo "And the snap is able to introspect hostname1" + expected="" + su -l -c "test-snapd-system-observe-consumer.dbus-introspect" test | MATCH "$expected" + fi + + if [ "$(snap debug confinement)" = strict ] ; then + echo "===========================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-system-observe-consumer:system-observe + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to get system information" + if su -l -c "test-snapd-system-observe-consumer.consumer 2>${PWD}/consumer.error" test; then + echo "Expected error with plug disconnected" + exit 1 + fi + cat consumer.error | MATCH "Permission denied" + + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + echo "And the snap is not able to introspect hostname1" + if su -l -c "test-snapd-system-observe-consumer.dbus-introspect 2>${PWD}/introspect.error" test; then + echo "Expected error with plug disconnected" + exit 1 + fi + cat introspect.error | MATCH "Permission denied" + fi + fi diff --git a/tests/main/interfaces-time-control/task.yaml b/tests/main/interfaces-time-control/task.yaml new file mode 100644 index 00000000..7b0cf3ae --- /dev/null +++ b/tests/main/interfaces-time-control/task.yaml @@ -0,0 +1,38 @@ +summary: Check that RTC device nodes are accessible through an interface + +details: | + This test makes sure that a snap using the time-control interface + can access the /dev/rtc device node exposed by a slot on the OS + snap properly. + +systems: [ubuntu-core-16-64] + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + echo "Given a snap declaring a plug on time-control is installed" + . $TESTSLIB/snaps.sh + install_local time-control-consumer + + echo "And the time-control plug is connected" + . $TESTSLIB/names.sh + snap connect time-control-consumer:time-control :time-control + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + . $TESTSLIB/dirs.sh + + # Read/write access should be possible + test -n "`$SNAP_MOUNT_DIR/bin/time-control-consumer.read`" + $SNAP_MOUNT_DIR/bin/time-control-consumer.write + + # Read/write access should be possible + test -n "`$SNAP_MOUNT_DIR/bin/time-control-consumer.timedatectl status`" + $SNAP_MOUNT_DIR/bin/time-control-consumer.timedatectl set-local-rtc no diff --git a/tests/main/interfaces-udev/task.yaml b/tests/main/interfaces-udev/task.yaml new file mode 100644 index 00000000..af838129 --- /dev/null +++ b/tests/main/interfaces-udev/task.yaml @@ -0,0 +1,33 @@ +summary: Ensure that the udev interface backend works. + +details: | + This test checks that the udev rules file is created when a snap declaring + a dependency on an interface (being it a slot or a plug) and that concrete + dependency has a related udev snippet, then the udev rules files are created + on install and removed after it is uninstalled. + + Currently the ony interface that declares a udev snippet is the modem-manager + interface for its slot. This test can be easily extended with variants when + more interfaces declare udev snippets. + +prepare: | + echo "Given a snap declaring a slot with associated udev rules is installed" + snap pack $TESTSLIB/snaps/modem-manager-consumer + snap install --dangerous modem-manager-consumer_1.0_all.snap + +restore: | + rm -f modem-manager-consumer_1.0_all.snap + +execute: | + echo "Then the udev rules files specific to it are created" + test -f /etc/udev/rules.d/70-snap.modem-manager-consumer.rules + expected="ATTRS{idVendor}==\".*?\", ATTRS{idProduct}==\".*?\"" + grep -Pq "$expected" /etc/udev/rules.d/70-snap.modem-manager-consumer.rules + + echo "=======================================" + + echo "When the snap is removed" + snap remove modem-manager-consumer + + echo "Then the udev rules files are removed" + ! test -f /etc/udev/rules.d/70-snap.modem-manager-consumer.rules diff --git a/tests/main/interfaces-uhid/task.yaml b/tests/main/interfaces-uhid/task.yaml new file mode 100644 index 00000000..11079e87 --- /dev/null +++ b/tests/main/interfaces-uhid/task.yaml @@ -0,0 +1,49 @@ +summary: Ensure that the uhid interface works. + +details: | + The uhid interface allows accessing the UHID to create kernel hid devices + + The test-snapd-uhid snap is able to create and destroy a device on /dev/uhid. + + The test checks the snap is able to manage devices on /dev/uhid, the + interface is not connected by default and it can be connected and disconnected. + +prepare: | + echo "Given the uhid snap is installed" + snap install test-snapd-uhid + +restore: | + rm -f call.error + +execute: | + CONNECTED_PATTERN=":uhid +test-snapd-uhid" + DISCONNECTED_PATTERN="\- +test-snapd-uhid:uhid" + + echo "The plug is not connected by default" + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "When the plug is connected" + snap connect test-snapd-uhid:uhid + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Exit when uhid is not supported, specially on cloud images" + if [ ! -d /dev/uhid ]; then + exit 0 + fi + + echo "Then the snap is able to create/destroy a device in /dev/uhid" + test-snapd-uhid.test-device + + if [ "$(snap debug confinement)" = partial ]; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-uhid:uhid + + echo "Then the snap is not able to create/destroy a device on /dev/uhid" + if test-snapd-uhid.test-device 2>${PWD}/call.error; then + echo "Expected permission error calling uhid with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < call.error diff --git a/tests/main/interfaces-upower-observe/task.yaml b/tests/main/interfaces-upower-observe/task.yaml new file mode 100644 index 00000000..d685c17e --- /dev/null +++ b/tests/main/interfaces-upower-observe/task.yaml @@ -0,0 +1,74 @@ +summary: Ensure that the upower-observe interface works. + +systems: + # ppc64el disabled because of https://github.com/snapcore/snapd/issues/2504 + - -ubuntu-*-ppc64el + - -fedora-* + - -opensuse-* + +details: | + The upower-observe interface allows a snap to query UPower for power devices, history + and statistics. + + A snap which defines the upower-observe plug must be shown in the interfaces list. + The plug must be autoconnected on install and, as usual, must be able to be reconnected. + + The test uses a snap wrapping the upower command line utility, and checks that it can query + it without error while the plug is connected. + +prepare: | + echo "Given a snap declaring a plug on the upower-observe interface is installed" + snap install --edge test-snapd-upower-observe-consumer + + if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then + echo "And a snap providing a upower-observe slot is installed" + snap install upower + else + . "$TESTSLIB/pkgdb.sh" + distro_install_package upower + fi + +restore: | + rm -f upower.error + if [[ "$SPREAD_SYSTEM" != ubuntu-core-* ]]; then + . "$TESTSLIB/pkgdb.sh" + distro_purge_package upower + fi + +execute: | + SLOT_PROVIDER= + SLOT_NAME=upower-observe + if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then + SLOT_PROVIDER=upower + SLOT_NAME=service + fi + + CONNECTED_PATTERN="$SLOT_PROVIDER:$SLOT_NAME +test-snapd-upower-observe-consumer" + DISCONNECTED_PATTERN="\- +test-snapd-upower-observe-consumer:upower-observe" + + echo "The snap is connected by default" + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "When the plug is connected the snap is able to dump info about the upower devices" + expected="/org/freedesktop/UPower/devices/DisplayDevice.*" + for i in $(seq 20); do + if ! test-snapd-upower-observe-consumer.upower --dump | MATCH "$expected"; then + sleep 1 + fi + done + test-snapd-upower-observe-consumer.upower --dump | MATCH "$expected" + + echo "===================================" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "When the plug is disconnected" + snap disconnect test-snapd-upower-observe-consumer:upower-observe + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to dump info about the upower devices" + if test-snapd-upower-observe-consumer.upower --dump 2>${PWD}/upower.error; then + echo "Expected permission error accessing upower info with disconnected plug" + exit 1 + fi + cat upower.error | MATCH "Permission denied" + fi diff --git a/tests/main/interfaces-wayland/task.yaml b/tests/main/interfaces-wayland/task.yaml new file mode 100644 index 00000000..d0de92f5 --- /dev/null +++ b/tests/main/interfaces-wayland/task.yaml @@ -0,0 +1,65 @@ +summary: Ensure that the wayland interface works + +# Only test on classic Ubuntu amd64 systems that have wayland +systems: [ ubuntu-1*-*64 ] + +prepare: | + . $TESTSLIB/pkgdb.sh + snap install --edge test-snapd-wayland + +restore: | + echo "Stop weston compositor" + /usr/bin/killall -9 /usr/bin/weston || true + +execute: | + CONNECTED_PATTERN=":wayland +test-snapd-wayland" + DISCONNECTED_PATTERN="\- +test-snapd-wayland:wayland" + + echo "When the plug is connected" + snap connect test-snapd-wayland:wayland + snap interfaces | MATCH "$CONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = "partial" ] ; then + exit 0 + fi + + echo "====================================" + + echo "Create XDG_RUNTIME_DIR=/run/user/12345" + mkdir -p /run/user/12345 || true + chmod 700 /run/user/12345 + chown test:test /run/user/12345 + + echo "Start weston compositor under test user" + XDG_RUNTIME_DIR=/run/user/12345 su -p -c "weston --backend=headless-backend.so" test & + + echo "Then wait for the socket to show up" + count=0 + while sleep 1 && [ ! -S /run/user/12345/wayland-0 ]; do + echo $count + count=$((count+1)) + if [ "$count" -gt 10 ]; then + echo "Could not find wayland socket" + exit 1 + fi + done + + echo "Then the snap command under the test user is able connect to the wayland socket" + XDG_RUNTIME_DIR=/run/user/12345 su -p -l -c test-snapd-wayland test | MATCH wl_compositor + + echo "====================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-wayland:wayland + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + + echo "Then the snap command is not able to connect to the wayland socket" + if XDG_RUNTIME_DIR=/run/user/12345 su -p -l -c test-snapd-wayland test; then + echo "Expected error with plug disconnected" + exit 1 + fi + + # If this is in 'restore', execute doesn't exit and spread must timeout + # the test. + echo "Stop weston compositor" + /usr/bin/killall -9 /usr/bin/weston || true diff --git a/tests/main/known-remote/task.yaml b/tests/main/known-remote/task.yaml new file mode 100644 index 00000000..0ceba7d0 --- /dev/null +++ b/tests/main/known-remote/task.yaml @@ -0,0 +1,8 @@ +summary: Check snap known --store +execute: | + echo "Check getting assertion from the store" + output=$(snap known --remote model series=16 brand-id=canonical model=pi2) + echo $output |MATCH "type: model" + echo $output |MATCH "series: 16" + echo $output |MATCH "brand-id: canonical" + echo $output |MATCH "model: pi2" diff --git a/tests/main/known/task.yaml b/tests/main/known/task.yaml new file mode 100644 index 00000000..99e2c469 --- /dev/null +++ b/tests/main/known/task.yaml @@ -0,0 +1,15 @@ +summary: Check snap known +execute: | + echo "Listing all account assertions" + snap known account|MATCH "^type: account$" + snap known account|MATCH "^account-id: canonical$" + + echo "Finding one account assertion with filters" + cnt=$(snap known account account-id=canonical|grep -c "^type: account$") + [ "$cnt" -eq 1 ] + snap known account|MATCH "^account-id: canonical$" + snap known account|MATCH "^username: canonical$" + + echo "Searching non existing assertion" + cnt=$(snap known account account-id=non-existing|grep -c "^type: account$" || true) + [ "$cnt" -eq 0 ] diff --git a/tests/main/listing/task.yaml b/tests/main/listing/task.yaml new file mode 100644 index 00000000..1660f423 --- /dev/null +++ b/tests/main/listing/task.yaml @@ -0,0 +1,41 @@ +summary: Check snap listings + +prepare: | + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + +execute: | + + echo "List prints core snap version" + # most core versions should be like "16-2", so [0-9]{2}-[0-9.]+ + # but edge will have a timestamp in there, "16.2+201701010932", so add an optional \+[0-9]+ to the end + # *current* edge also has .git. and a hash snippet, so add an optional .git.[0-9a-f]+ to the already optional timestamp + if [ "$SPREAD_BACKEND" = "linode" -o "$SPREAD_BACKEND" == "qemu" ] && [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then + echo "With customized images the core snap is sideloaded" + expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +x[0-9]+ +core *$' + elif [ "$SRU_VALIDATION" = "1" ]; then + echo "When sru validation is done the core snap is installed from the store" + expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+[0-9]+\.[0-9a-f]+)? +[0-9]+ +canonical +core *$' + else + expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +[0-9]+ +canonical +core *$' + fi + snap list | MATCH "$expected" + + echo "List prints installed snap version" + snap list | MATCH '^test-snapd-tools +[0-9]+(\.[0-9]+)* +x[0-9]+ +- *$' + + echo "Install test-snapd-tools again" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + echo "And run snap list --all" + output=$(snap list --all |grep test-snapd-tools) + if [ "$(grep -c test-snapd-tools <<< "$output")" != "2" ]; then + echo "Expected two test-snapd-tools in the output, got:" + echo $output + exit 1 + fi + if [ "$(grep -c disabled <<< "$output")" != "1" ]; then + echo "Expected one disabled line in in the output, got:" + echo $output + exit 1 + fi diff --git a/tests/main/local-install-w-metadata/digest.go b/tests/main/local-install-w-metadata/digest.go new file mode 100644 index 00000000..0a2a8639 --- /dev/null +++ b/tests/main/local-install-w-metadata/digest.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "os" + + "github.com/snapcore/snapd/asserts" +) + +func main() { + sha3_384, _, err := asserts.SnapFileSHA3_384(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot compute digest: %v\n", err) + os.Exit(1) + } + fmt.Fprintf(os.Stdout, "%s\n", sha3_384) +} diff --git a/tests/main/local-install-w-metadata/task.yaml b/tests/main/local-install-w-metadata/task.yaml new file mode 100644 index 00000000..9a06286e --- /dev/null +++ b/tests/main/local-install-w-metadata/task.yaml @@ -0,0 +1,23 @@ +summary: Checks for local install with metadata from assertions +# XXX we would need to bother with curl there atm +systems: [-ubuntu-core-16-*] +restore: | + rm -f test-snapd-tools_*.{snap,assert} +execute: | + echo "Get the snap" + snap download test-snapd-tools + + echo "Try to install the snap without assertions" + (snap install test-snapd-tools_*.snap 2>&1 || true) | MATCH 'cannot find signatures with metadata for snap "test-snapd-tools.*\.snap"' + + echo "Add its assertions" + snap ack test-snapd-tools_*.assert + + echo "Installing the snap file will use the metadata from assertions" + snap install test-snapd-tools_*.snap + + echo "The revision is not a local revision" + snap list|MATCH '^test-snapd-tools.* [0-9]+\s+canonical' + + echo "Test it" + test-snapd-tools.success diff --git a/tests/main/login/missing_email_error.exp b/tests/main/login/missing_email_error.exp new file mode 100644 index 00000000..28c0364d --- /dev/null +++ b/tests/main/login/missing_email_error.exp @@ -0,0 +1,9 @@ +spawn snap login + +expect { + "Email address:" { + exit 0 + } default { + exit 1 + } +} diff --git a/tests/main/login/successful_login.exp b/tests/main/login/successful_login.exp new file mode 100644 index 00000000..fcb4c8c7 --- /dev/null +++ b/tests/main/login/successful_login.exp @@ -0,0 +1,13 @@ +log_user 0 +spawn snap login $env(SPREAD_STORE_USER) + +expect "Password of " +send "$env(SPREAD_STORE_PASSWORD)\n" + +expect { + "Login successful" { + exit 0 + } default { + exit 1 + } +} diff --git a/tests/main/login/task.yaml b/tests/main/login/task.yaml new file mode 100644 index 00000000..67302a4b --- /dev/null +++ b/tests/main/login/task.yaml @@ -0,0 +1,33 @@ +summary: Checks for snap login + +# ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] + +restore: | + snap logout || true + +execute: | + echo "Checking missing email error" + expect -d -f missing_email_error.exp + + echo "Checking wrong password error" + expect -d -f unsuccessful_login.exp + + output=$(snap managed) + if [ "$output" != "false" ]; then + echo "Unexpected output from 'snap managed': $output" + exit 1 + fi + + if [ -n "$SPREAD_STORE_USER" ] && [ -n "$SPREAD_STORE_PASSWORD" ]; then + echo "Checking successful login" + expect -d -f successful_login.exp + + output=$(snap managed) + if [ "$output" != "true" ]; then + echo "Unexpected output from 'snap managed': $output" + exit 1 + fi + + snap logout + fi diff --git a/tests/main/login/unsuccessful_login.exp b/tests/main/login/unsuccessful_login.exp new file mode 100644 index 00000000..671bf34f --- /dev/null +++ b/tests/main/login/unsuccessful_login.exp @@ -0,0 +1,14 @@ +set timeout 60 + +spawn snap login someemail@testing.com + +expect "Password of " +send "wrong-password\n" + +expect { + -re "invalid\[ \n\r\]*credentials" { + exit 0 + } default { + exit 1 + } +} diff --git a/tests/main/lxd/task.yaml b/tests/main/lxd/task.yaml new file mode 100644 index 00000000..bb357afb --- /dev/null +++ b/tests/main/lxd/task.yaml @@ -0,0 +1,92 @@ +summary: Ensure that lxd works + +# only run this on ubuntu 16+, lxd will not work on !ubuntu systems +# currently nor on ubuntu 14.04 +systems: [ubuntu-16*, ubuntu-core-*] + +# lxd downloads can be quite slow +kill-timeout: 25m + +restore: | + if [[ $(ls -1 "$GOHOME"/snapd_*.deb | wc -l || echo 0) -eq 0 ]]; then + exit + fi + + lxd.lxc stop my-ubuntu + lxd.lxc delete my-ubuntu + +debug: | + # debug output from lxd + journalctl -u snap.lxd.daemon.service + +execute: | + if [[ $(ls -1 "$GOHOME"/snapd_*.deb | wc -l || echo 0) -eq 0 ]]; then + echo "No run lxd test when there are not .deb files built" + exit + fi + + wait_for_lxd(){ + while ! printf "GET / HTTP/1.0\n\n" | nc -U /var/snap/lxd/common/lxd/unix.socket | MATCH "200 OK"; do sleep 1; done + } + + echo "Install lxd" + snap install lxd + + echo "Create a trivial container using the lxd snap" + wait_for_lxd + lxd init --auto + + echo "Setting up proxy for lxc" + if [ -n "${http_proxy:-}" ]; then + lxd.lxc config set core.proxy_http $http_proxy + fi + if [ -n "${https_proxy:-}" ]; then + lxd.lxc config set core.proxy_https $http_proxy + fi + + lxd.lxc launch ubuntu:16.04 my-ubuntu + + echo "Ensure we can run things inside" + lxd.lxc exec my-ubuntu echo hello | MATCH hello + + echo "Ensure we can get network" + lxd.lxc network create testbr0 + lxd.lxc network attach testbr0 my-ubuntu eth0 + lxd.lxc exec my-ubuntu dhclient eth0 + + echo "Cleanup container" + lxd.lxc exec my-ubuntu -- apt autoremove --purge -y snapd ubuntu-core-launcher + + echo "Install snapd" + lxd.lxc exec my-ubuntu -- mkdir -p "$GOHOME" + lxd.lxc file push "$GOHOME"/snapd_*.deb my-ubuntu/$GOPATH/ + lxd.lxc exec my-ubuntu -- dpkg -i "$GOHOME"/snapd_*.deb + + echo "Setting up proxy *inside* the container" + if [ -n "${http_proxy:-}" ]; then + lxd.lxc exec my-ubuntu -- sh -c "echo http_proxy=$http_proxy >> /etc/environment" + fi + if [ -n "${https_proxy:-}" ]; then + lxd.lxc exec my-ubuntu -- sh -c "echo https_proxy=$https_proxy >> /etc/environment" + fi + lxd.lxc exec my-ubuntu -- systemctl daemon-reload + lxd.lxc exec my-ubuntu -- systemctl restart snapd.service + lxd.lxc exec my-ubuntu -- cat /etc/environment + + # FIXME: workaround for missing squashfuse + lxd.lxc exec my-ubuntu apt update + lxd.lxc exec my-ubuntu -- apt install -y squashfuse + + # FIXME: ensure that the kernel running is recent enough, this + # will only work with an up-to-date xenial kernel (4.4.0-78+) + + echo "Ensure we can use snapd inside lxd" + lxd.lxc exec my-ubuntu snap install test-snapd-tools + echo "And we can run snaps as regular users" + lxd.lxc exec my-ubuntu -- su -c "/snap/bin/test-snapd-tools.echo from-the-inside" ubuntu | MATCH from-the-inside + echo "And as root" + lxd.lxc exec my-ubuntu -- test-snapd-tools.echo from-the-inside | MATCH from-the-inside + + echo "Install lxd-demo server to exercise the lxd interface" + snap install lxd-demo-server + snap connect lxd-demo-server:lxd lxd:lxd diff --git a/tests/main/manpages/task.yaml b/tests/main/manpages/task.yaml new file mode 100644 index 00000000..4040455d --- /dev/null +++ b/tests/main/manpages/task.yaml @@ -0,0 +1,17 @@ +summary: the essential manual pages are installed by the native package +# core systems don't ship man or manual pages +systems: [-ubuntu-core-16-*] +prepare: | + . "$TESTSLIB/pkgdb.sh" + distro_install_package man +restore: | + . "$TESTSLIB/pkgdb.sh" + distro_purge_package man +execute: | + for manpage in snap snap-confine snap-discard-ns; do + if ! LC_ALL=C man --what $manpage; then + echo "Expected to see manual page for $manpage" + exit 1 + fi + done +# TODO: add manual pages for snapctl, snap-exec and snapd diff --git a/tests/main/media-sharing/task.yaml b/tests/main/media-sharing/task.yaml new file mode 100644 index 00000000..09300d32 --- /dev/null +++ b/tests/main/media-sharing/task.yaml @@ -0,0 +1,21 @@ +summary: The /media directory propagates events outwards +details: | + The /media directory is special in that mount events propagate outward from + the mount namespace used by snap applications into the main mount + namespace. +prepare: | + . $TESTSLIB/snaps.sh + install_local_devmode test-snapd-tools + mkdir -p /media/src + mkdir -p /media/dst + touch /media/src/canary +execute: | + test ! -e /media/dst/canary + test-snapd-tools.cmd mount --bind /media/src /media/dst + test -e /media/dst/canary +restore: | + # If this doesn't work maybe it is because the test didn't execute correctly + umount /media/dst || true + rm -f /media/src/canary + rmdir /media/src + rmdir /media/dst diff --git a/tests/main/nfs-support/task.yaml b/tests/main/nfs-support/task.yaml new file mode 100644 index 00000000..f28621cc --- /dev/null +++ b/tests/main/nfs-support/task.yaml @@ -0,0 +1,123 @@ +summary: Test that snaps still work when /home is a NFS mount +details: | + Snapd now contains a feature where NFS-mounted /home (or any sub-directory) + initializes a workaround mode where all snaps gain minimal amount of network + permissions sufficient for NFS to operate. +systems: [ubuntu-16.04-64] # TODO: expand this list +prepare: | + . "$TESTSLIB/pkgdb.sh" + . "$TESTSLIB/snaps.sh" + + # Install NFS server and a simple shell snap. + distro_install_package nfs-kernel-server + install_local test-snapd-sh +execute: | + ensure_extra_perms() { + MATCH 'network inet,' < /var/lib/snapd/apparmor/snap-confine/nfs-support + MATCH 'network inet,' < /var/lib/snapd/apparmor/profiles/snap.test-snapd-sh.with-home-plug + } + + ensure_normal_perms() { + test ! -e /var/lib/snapd/apparmor/snap-confine/nfs-support + MATCH -v 'network inet,' < /var/lib/snapd/apparmor/profiles/snap.test-snapd-sh.with-home-plug + } + + # Export /home over NFS. + mkdir -p /etc/exports.d/ + echo '/home localhost(rw,no_subtree_check,no_root_squash)' > /etc/exports.d/test.exports + systemctl restart nfs-kernel-server + exportfs -r + + # Ensure that apparmor profiles don't permit network access + ensure_normal_perms + + # Mount NFS-exported /home over real /home using NFSv3 and TCP transport + mount -t nfs localhost:/home /home -o nfsvers=3,proto=tcp + + # Restart snapd to observe the active NFS mount. + systemctl restart snapd + + # Ensure that snap-confine's apparmor profile and the test snap's apparmor + # profile now permit network access. + ensure_extra_perms + + # As a non-root user perform a write over NFS-mounted /home + su -c 'snap run test-snapd-sh.with-home-plug -c "touch \$SNAP_USER_DATA/smoke-nfs3-tcp"' test + + # Unmount /home and restart snapd so that we can check another thing. + umount /home + systemctl restart snapd + + # Ensure that this removed the extra permissions. + ensure_normal_perms + + # Mount NFS-exported /home over real /home using NFSv3 and UDP transport + mount -t nfs localhost:/home /home -o nfsvers=3,proto=udp + + # Restart snapd to observe the active NFS mount. + systemctl restart snapd + + # Ensure that snap-confine's apparmor profile and the test snap's apparmor + # profile now permit network access. + ensure_extra_perms + + # As a non-root user perform a write over NFS-mounted /home + su -c 'snap run test-snapd-sh.with-home-plug -c "touch \$SNAP_USER_DATA/smoke-nfs3-udp"' test + + # Unmount /home and restart snapd so that we can check another thing. + umount /home + systemctl restart snapd + + # Ensure that this removed the extra permissions. + ensure_normal_perms + + # Mount NFS-exported /home over real /home using NFSv4 + mount -t nfs localhost:/home /home -o nfsvers=4 + + # Restart snapd to observe the active NFS mount. + systemctl restart snapd + + # Ensure that snap-confine's apparmor profile and the test snap's apparmor + # profile now permit network access. + ensure_extra_perms + + # As a non-root user perform a write over NFS-mounted /home + su -c 'snap run test-snapd-sh.with-home-plug -c "touch \$SNAP_USER_DATA/smoke-nfs4"' test + + # Unmount /home and restart snapd so that we can check another thing. + umount /home + systemctl restart snapd + + # Ensure that this removed the extra permissions. + ensure_normal_perms + + # Back up the /etc/fstab file and define a NFS mount mount there. + cp /etc/fstab fstab.orig + echo 'localhost:/home /home nfs defaults 0 0' >> /etc/fstab + + # Restart snapd and ensure that we have extra permissions again. + # + # Note that at this time /home is not mounted as NFS yet but the mere + # presence of the entry in /etc/fstab is sufficient to grant extra + # permissions. + systemctl restart snapd + ensure_extra_perms +restore: | + . "$TESTSLIB/pkgdb.sh" + + # Unmount NFS mount over /home if one exists. + umount /home || true + + # Restore the fstab backup file if one exists. + if [ -e fstab.orig ]; then + mv fstab.orig /etc/fstab + fi + + # Remove the NFS server and its configuration data. + rm -f /etc/exports.d/test.exports + rm -f -d /etc/exports.d + exportfs -r + distro_purge_package nfs-kernel-server bsdgames + + # Restart snapd in to ensure it doesn't know about NFS anymore. + systemctl restart snapd.service diff --git a/tests/main/op-install-failed-undone/task.yaml b/tests/main/op-install-failed-undone/task.yaml new file mode 100644 index 00000000..3ae772e1 --- /dev/null +++ b/tests/main/op-install-failed-undone/task.yaml @@ -0,0 +1,53 @@ +summary: Check that all tasks of a failed installtion are undone + +systems: [-ubuntu-core-16-*] + +restore: | + . $TESTSLIB/dirs.sh + rm -rf $SNAP_MOUNT_DIR/test-snapd-tools + +execute: | + check_empty_glob(){ + local base_path=$1 + local glob=$2 + [ $(find $base_path -maxdepth 1 -name "$glob" | wc -l) -eq 0 ] + } + + . $TESTSLIB/dirs.sh + + echo "Given we make a snap uninstallable" + mkdir -p $SNAP_MOUNT_DIR/test-snapd-tools/current/foo + + echo "And we try to install it" + . $TESTSLIB/snaps.sh + if install_local test-snapd-tools; then + echo "A snap shouldn't be installable if its mount point is busy" + exit 1 + fi + + echo "Then the snap isn't installed" + snap list | MATCH -v test-snapd-tools + + echo "And the installation task is reported as an error" + failed_task_id=$(snap changes | perl -ne 'print $1 if /(\d+) +Error.*?Install \"test-snapd-tools\" snap/') + if [ -z $failed_task_id ]; then + echo "Installation task should be reported as error" + exit 1 + fi + + echo "And the Mount subtask is actually undone" + snap change $failed_task_id | grep -Pq "Undone +.*?Mount snap \"test-snapd-tools\"" + check_empty_glob $SNAP_MOUNT_DIR/test-snapd-tools [0-9]+ + check_empty_glob /var/lib/snapd/snaps test-snapd-tools_[0-9]+.snap + + echo "And the Data Copy subtask is actually undone" + snap change $failed_task_id | grep -Pq "Undone +.*?Copy snap \"test-snapd-tools\" data" + check_empty_glob $HOME/snap/test-snapd-tools [0-9]+ + check_empty_glob /var/snap/test-snapd-tools [0-9]+ + + echo "And the Security Profiles Setup subtask is actually undone" + snap change $failed_task_id | grep -Pq "Undone +.*?Setup snap \"test-snapd-tools\" \(unset\) security profiles" + check_empty_glob /var/lib/snapd/apparmor/profiles snap.test-snapd-tools.* + check_empty_glob /var/lib/snapd/seccomp/bpf snap.test-snapd-tools.* + check_empty_glob /etc/dbus-1/system.d snap.test-snapd-tools.*.conf + check_empty_glob /etc/udev/rules.d 70-snap.test-snapd-tools.*.rules diff --git a/tests/main/op-remove-retry/task.yaml b/tests/main/op-remove-retry/task.yaml new file mode 100644 index 00000000..3c8b6765 --- /dev/null +++ b/tests/main/op-remove-retry/task.yaml @@ -0,0 +1,42 @@ +summary: Check that a remove operation is working even if the mount point is busy. + +restore: | + kill %1 || true + +execute: | + wait_for_remove_state(){ + local state=$1 + local expected="(?s)$state.*?Remove \"test-snapd-tools\" snap" + while ! snap changes | grep -Pq "$expected"; do sleep 1; done + } + + . $TESTSLIB/systemd.sh + + echo "Given a snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + + echo "And its mount point is kept busy" + # we need a marker file, because just using systemd to figure out + # if the service has started is racy, start just means started, + # not that the dir is actually blocked yet + MARKER=/var/snap/test-snapd-tools/current/block-running + rm -f $MARKER + + systemd_create_and_start_unit unmount-blocker "$(which test-snapd-tools.block)" + + wait_for_service unmount-blocker active + while [ ! -f $MARKER ]; do sleep 1; done + + echo "When we try to remove the snap" + snap remove test-snapd-tools & + + echo "Then the remove retry succeeds" + wait_for_remove_state Done + + echo "And the snap is removed" + while snap list | grep -q test-snapd-tools; do sleep 1; done + + # cleanup umount blocker + systemd_stop_and_destroy_unit unmount-blocker + diff --git a/tests/main/op-remove/task.yaml b/tests/main/op-remove/task.yaml new file mode 100644 index 00000000..a30a3813 --- /dev/null +++ b/tests/main/op-remove/task.yaml @@ -0,0 +1,32 @@ +summary: Check snap remove operations. + +restore: | + rm -f basic_1.0_all.snap + rm -f stderr.out + +execute: | + . $TESTSLIB/dirs.sh + + snap_revisions(){ + local snap_name=$1 + echo -n $(find $SNAP_MOUNT_DIR/"$snap_name"/ -maxdepth 1 -type d -name "x*" | wc -l) + } + + echo "Given two revisions of a snap have been installed" + snap pack $TESTSLIB/snaps/basic + snap install --dangerous basic_1.0_all.snap + snap install --dangerous basic_1.0_all.snap + + echo "Then the two revisions are available on disk" + [ $(snap_revisions basic) = "2" ] + + echo "When the snap is removed" + snap remove basic + + echo "Then the two revisions are removed from disk" + [ $(snap_revisions basic) = "0" ] + + echo "When the snap is removed again, snap exits with status 0" + snap remove basic 2> stderr.out + cat stderr.out | MATCH 'snap "basic" is not installed' + diff --git a/tests/main/postrm-purge/task.yaml b/tests/main/postrm-purge/task.yaml new file mode 100644 index 00000000..017f2105 --- /dev/null +++ b/tests/main/postrm-purge/task.yaml @@ -0,0 +1,37 @@ +summary: Check that postrm purge works + +systems: [-ubuntu-core-16-*] + +execute: | + echo "When some snaps are installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + snap install test-snapd-control-consumer + snap install test-snapd-auto-aliases + + . $TESTSLIB/dirs.sh + + # For now we use the Fedora specific snap-mgmt script but as soon + # as we have a generic one we can use cross-distro we need to + # change this. + echo "And snapd is purged" + if [[ "$SPREAD_SYSTEM" = fedora-* ]] ; then + sh ${SPREAD_PATH}/packaging/fedora/snap-mgmt.sh \ + --snap-mount-dir=$SNAP_MOUNT_DIR \ + --purge + else + # only available on trusty + if [ -x ${SPREAD_PATH}/debian/snapd.prerm ]; then + sh -x ${SPREAD_PATH}/debian/snapd.prerm + fi + sh -x ${SPREAD_PATH}/debian/snapd.postrm purge + fi + + echo "Nothing is left" + for d in $SNAP_MOUNT_DIR /var/snap; do + if [ -d "$d" ]; then + echo "$d is not removed" + ls -lR $d + exit 1 + fi + done diff --git a/tests/main/prefer/task.yaml b/tests/main/prefer/task.yaml new file mode 100644 index 00000000..53554314 --- /dev/null +++ b/tests/main/prefer/task.yaml @@ -0,0 +1,34 @@ +summary: Simple snap prefer test +execute: | + . $TESTSLIB/dirs.sh + + echo "Install the snap with auto-aliases" + snap install test-snapd-auto-aliases + + echo "Sanity check" + test -h $SNAP_MOUNT_DIR/bin/test_snapd_wellknown1 + test -h $SNAP_MOUNT_DIR/bin/test_snapd_wellknown2 + + echo "Disable the auto-aliases" + snap unalias test-snapd-auto-aliases + + echo "Auto-aliases are gone" + test ! -e $SNAP_MOUNT_DIR/bin/test_snapd_wellknown1 + test ! -e $SNAP_MOUNT_DIR/bin/test_snapd_wellknown2 + + echo "Check listing" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown1 +test_snapd_wellknown1 +disabled" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2 +disabled" + + echo "Execute snap prefer" + snap prefer test-snapd-auto-aliases|MATCH ".*- test-snapd-auto-aliases.wellknown1 as test_snapd_wellknown1.*" + + echo "Test that the auto-aliases are back" + test -h $SNAP_MOUNT_DIR/bin/test_snapd_wellknown1 + test -h $SNAP_MOUNT_DIR/bin/test_snapd_wellknown2 + test_snapd_wellknown1|MATCH "ok wellknown 1" + test_snapd_wellknown2|MATCH "ok wellknown 2" + + echo "Check listing" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown1 +test_snapd_wellknown1 +-" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2 +-" diff --git a/tests/main/prepare-image-grub/task.yaml b/tests/main/prepare-image-grub/task.yaml new file mode 100644 index 00000000..8c5585ca --- /dev/null +++ b/tests/main/prepare-image-grub/task.yaml @@ -0,0 +1,84 @@ +summary: Check that prepare-image works for grub-systems + +systems: [-ubuntu-core-16-*, -fedora-*, -opensuse-*] + +backends: [-autopkgtest] + +# TODO: use the real stores with proper assertions fully as well once possible +environment: + ROOT: /tmp/root + IMAGE: /tmp/root/image + GADGET: /tmp/root/gadget + STORE_DIR: $(pwd)/fake-store-blobdir + STORE_ADDR: localhost:11028 + UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_SNAPS: 1 + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + . $TESTSLIB/store.sh + setup_fake_store $STORE_DIR + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + . $TESTSLIB/store.sh + teardown_fake_store $STORE_DIR + rm -rf $ROOT + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + echo Expose the needed assertions through the fakestore + cp $TESTSLIB/assertions/developer1.account $STORE_DIR/asserts + cp $TESTSLIB/assertions/developer1.account-key $STORE_DIR/asserts + # have snap use the fakestore for assertions (but nothing else) + export SNAPPY_FORCE_SAS_URL=http://$STORE_ADDR + + echo Running prepare-image + su -c "SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE snap prepare-image --channel edge --extra-snaps snapweb $TESTSLIB/assertions/developer1-pc.model $ROOT" test + + echo Verifying the result + ls -lR $IMAGE + for f in pc pc-kernel core snapweb; do + ls $IMAGE/var/lib/snapd/seed/snaps/${f}*.snap + done + MATCH snap_core=core < $IMAGE/boot/grub/grubenv + MATCH snap_kernel=pc-kernel < $IMAGE/boot/grub/grubenv + + # check copied assertions + cmp $TESTSLIB/assertions/developer1-pc.model $IMAGE/var/lib/snapd/seed/assertions/model + cmp $TESTSLIB/assertions/developer1.account $IMAGE/var/lib/snapd/seed/assertions/developer1.account + + echo Verify the unpacked gadget + ls -lR $GADGET + ls $GADGET/meta/snap.yaml + + echo "Verify that we have valid looking seed.yaml" + cat $IMAGE/var/lib/snapd/seed/seed.yaml + + # snap-id of core + if [ "$REMOTE_STORE" = production ]; then + core_snap_id="99T7MUlRhtI3U0QFgl5mXXESAiSwt776" + else + core_snap_id="xMNMpEm0COPZy7jq9YRwWVLCD9q5peow" + fi + MATCH "snap-id: ${core_snap_id}" < $IMAGE/var/lib/snapd/seed/seed.yaml + + for snap in pc pc-kernel core; do + MATCH "name: $snap" < $IMAGE/var/lib/snapd/seed/seed.yaml + done + + echo "Verify that we got some snap assertions" + for name in pc pc-kernel core; do + cat $IMAGE/var/lib/snapd/seed/assertions/* | MATCH "snap-name: $name" + done diff --git a/tests/main/prepare-image-uboot/task.yaml b/tests/main/prepare-image-uboot/task.yaml new file mode 100644 index 00000000..a68c1853 --- /dev/null +++ b/tests/main/prepare-image-uboot/task.yaml @@ -0,0 +1,69 @@ +summary: Check that prepare-image works for uboot-systems +environment: + ROOT: /tmp/root + IMAGE: /tmp/root/image + GADGET: /tmp/root/gadget +prepare: | + mkdir -p $ROOT + chown test:test $ROOT +restore: | + rm -rf $ROOT +execute: | + # TODO: switch to a prebuilt properly signed model assertion once we can do that consistently + echo Creating model assertion + cat > $ROOT/model.assertion <&1 | MATCH "All snaps up to date" + + echo "When the store is configured to make them refreshable" + . $TESTSLIB/files.sh + . $TESTSLIB/store.sh + init_fake_refreshes "$BLOB_DIR" "$GOOD_SNAP" + wait_for_file "$BLOB_DIR"/"${GOOD_SNAP}"*fake1*.snap 4 .5 + init_fake_refreshes "$BLOB_DIR" "$BAD_SNAP" + wait_for_file "$BLOB_DIR"/"${BAD_SNAP}"*fake1*.snap 4 .5 + + echo "When a snap is broken" + echo "i-am-broken-now" >> $BLOB_DIR/${BAD_SNAP}*fake1*.snap + + echo "And a refresh is performed" + if snap refresh ; then + echo "snap refresh should fail but it did not, test is broken" + exit 1 + fi + + echo "Then the new version of the good snap got installed" + snap list | MATCH -E "${GOOD_SNAP}.*?fake1" + + echo "But the bad snap did not get updated" + snap list | MATCH -E "${BAD_SNAP}"| MATCH -v "fake" + + . $TESTSLIB/changes.sh + chg_id=$(change_id "Refresh snap" Error) + + echo "Verify the snap change" + snap change $chg_id | MATCH "Undone.*Download snap \"${BAD_SNAP}\"" + snap change $chg_id | MATCH "Done.*Download snap \"${GOOD_SNAP}\"" + snap change $chg_id | MATCH "ERROR cannot verify snap \"test-snapd-tools\", no matching signatures found" + + echo "Verify the 'snap tasks' is the same as 'snap change'" + snap tasks $chg_id | MATCH "Undone.*Download snap \"${BAD_SNAP}\"" + + echo "Verify the 'snap tasks --last' shows last refresh change" + snap tasks --last=refresh | MATCH "Undone.*Download snap \"${BAD_SNAP}\"" diff --git a/tests/main/refresh-all/task.yaml b/tests/main/refresh-all/task.yaml new file mode 100644 index 00000000..e0634050 --- /dev/null +++ b/tests/main/refresh-all/task.yaml @@ -0,0 +1,62 @@ +summary: Check that more than one snap is refreshed. + +systems: [-ubuntu-core-16-*] + +details: | + We use only the fake store for this test because we currently + have only one controlled snap in the remote stores, when we will + have more we can update the test to use them + +environment: + BLOB_DIR: $(pwd)/fake-store-blobdir + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + . $TESTSLIB/store.sh + + echo "Given two snaps are installed" + for snap in test-snapd-tools test-snapd-python-webserver; do + snap install $snap + done + + echo "And the daemon is configured to point to the fake store" + setup_fake_store $BLOB_DIR + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + . $TESTSLIB/store.sh + teardown_fake_store $BLOB_DIR + rm -rf $BLOB_DIR + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + echo "Sanity check for the fake store" + snap refresh 2>&1 | MATCH "All snaps up to date." + + echo "When the store is configured to make them refreshable" + . $TESTSLIB/files.sh + . $TESTSLIB/store.sh + init_fake_refreshes $BLOB_DIR test-snapd-tools + wait_for_file $BLOB_DIR/test-snapd-tools*fake1*.snap 4 .5 + init_fake_refreshes $BLOB_DIR test-snapd-python-webserver + wait_for_file $BLOB_DIR/test-snapd-python-webserver*fake1*.snap 4 .5 + + echo "And a refresh is performed" + snap refresh + + echo "Then the new versions are installed" + for snap in test-snapd-tools test-snapd-python-webserver; do + snap list | MATCH "$snap.*fake1" + done diff --git a/tests/main/refresh-delta-from-core/task.yaml b/tests/main/refresh-delta-from-core/task.yaml new file mode 100644 index 00000000..c80c6eb0 --- /dev/null +++ b/tests/main/refresh-delta-from-core/task.yaml @@ -0,0 +1,27 @@ +summary: Check refresh works with xdelta3 from the core snap + +# delta downloads are currently disabled by default on core +systems: [-ubuntu-core-*] + +environment: + SNAP_NAME: test-snapd-delta-refresh + +prepare: | + echo "Ensure no xdelta3 available on the host" + if [ -e /usr/bin/xdelta3 ]; then + mv /usr/bin/xdelta3{,.disabled} + fi + + echo "Given a snap is installed" + snap install --edge $SNAP_NAME + +restore: | + if [ -e /usr/bin/xdelta3.disabled ]; then + mv /usr/bin/xdelta3{.disabled,} + fi + +execute: | + echo "When the snap is refreshed" + snap refresh --beta $SNAP_NAME + echo "Then deltas are successfully applied" + journalctl -u snapd | MATCH "Successfully applied delta" diff --git a/tests/main/refresh-delta/task.yaml b/tests/main/refresh-delta/task.yaml new file mode 100644 index 00000000..7632a833 --- /dev/null +++ b/tests/main/refresh-delta/task.yaml @@ -0,0 +1,26 @@ +summary: Check that the refresh command uses deltas + +# delta downloads are currently disabled by default on core +systems: [-ubuntu-core-*] + +environment: + SNAP_NAME: test-snapd-delta-refresh + SNAP_VERSION_PATTERN: \d+\.\d+\+fake1 + +prepare: | + # The store currently only calculates deltas in the same channel, + # so we need to setup the test first with two edge uploads, then + # set on of the edge snaps to beta. This was done with r3 -> r5. + # + # We have edge as r3, beta as r5 and the store has a delta for + # r3 -> r5b + # + echo "Given a snap is installed" + snap install --edge $SNAP_NAME + +execute: | + echo "When the snap is refreshed" + snap refresh --beta $SNAP_NAME + + echo "Then deltas are successfully applied" + journalctl -u snapd | MATCH "Successfully applied delta" diff --git a/tests/main/refresh-devmode/task.yaml b/tests/main/refresh-devmode/task.yaml new file mode 100644 index 00000000..9cdcee07 --- /dev/null +++ b/tests/main/refresh-devmode/task.yaml @@ -0,0 +1,77 @@ +summary: Check that the refresh command works. +details: | + These tests exercise the refresh command using different store backends. + The concrete store to be used is controlled with the STORE_TYPE variant, + the defined values are fake, for a local store, or remote, for the currently + configured remote store. + When executing against the remote stores the tests rely in the existence of + a given snap with an updatable version (version string like 2.0+fake1) in the + edge channel. + +environment: + SNAP_NAME: test-snapd-tools + SNAP_VERSION_PATTERN: \d+\.\d+\+fake1 + BLOB_DIR: $(pwd)/fake-store-blobdir + STORE_TYPE/fake: fake + STORE_TYPE/remote: ${REMOTE_STORE} + +prepare: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + fi + + echo "Given a snap is installed" + snap install --devmode test-snapd-tools + + if [ "$STORE_TYPE" = "fake" ]; then + . $TESTSLIB/store.sh + setup_fake_store $BLOB_DIR + + echo "And a new version of that snap put in the controlled store" + . $TESTSLIB/store.sh + init_fake_refreshes $BLOB_DIR test-snapd-tools + fi + +restore: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/store.sh + teardown_fake_store $BLOB_DIR + fi + +execute: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + fi + + # FIXME: currently the --list from channel doesn't work + # echo "Then the new version is available for the snap to be refreshed" + # expected="$SNAP_NAME +$SNAP_VERSION_PATTERN" + # snap refresh --list | grep -Pzq "$expected" + # + # echo "=================================" + + echo "When the snap is refreshed" + snap refresh --devmode --channel=edge $SNAP_NAME + + echo "Then the new version is listed" + expected="$SNAP_NAME +$SNAP_VERSION_PATTERN .*devmode" + snap list | grep -Pzq "$expected" diff --git a/tests/main/refresh-undo/task.yaml b/tests/main/refresh-undo/task.yaml new file mode 100644 index 00000000..81d80092 --- /dev/null +++ b/tests/main/refresh-undo/task.yaml @@ -0,0 +1,48 @@ +summary: Check that the undo on refresh keeps the previous snap intact +details: | + When a snap is refreshed and the refresh fails, the undo code had + a bug that removed the security confinement (LP: #1637981) + +# trusty has unreliable journalctl output for unknown reasonsg +systems: [-ubuntu-14.04-*] + +environment: + SNAP_NAME: test-snapd-service + SNAP_NAME_GOOD: ${SNAP_NAME}-v1-good + SNAP_NAME_BAD: ${SNAP_NAME}-v2-bad + SNAP_FILE_GOOD: ${SNAP_NAME}_1.0_all.snap + SNAP_FILE_BAD: ${SNAP_NAME}_2.0_all.snap + +prepare: | + echo "Given a good (v1) and a bad (v2) snap" + snap pack $TESTSLIB/snaps/$SNAP_NAME_GOOD + snap pack $TESTSLIB/snaps/$SNAP_NAME_BAD + +debug: | + journalctl -u snap.test-snapd-service.service.service + +execute: | + wait_for_service_status() { + retries=0 + while ! systemctl status snap.test-snapd-service.service.service|grep "$1"; do + # retry + retries=$((retries+1)) + if [ $retries -gt 30 ]; then + echo 'expected "service v1" output did not appear in systemctl status snap.test-snapd-service.service.service' + exit 1 + fi + sleep 1 + done + } + echo "When we install v1" + snap install --dangerous ${SNAP_FILE_GOOD} + echo "The v1 service started correctly" + wait_for_service_status "service v1" + + echo "When we refresh to v2" + if snap install --dangerous ${SNAP_FILE_BAD}; then + echo "The ${SNAP_FILE_BAD} snap should not install cleanly, test broken" + exit 1 + fi + echo "Then v2 is rolled back and v1 is started again" + wait_for_service_status "service v1" diff --git a/tests/main/refresh/task.yaml b/tests/main/refresh/task.yaml new file mode 100644 index 00000000..35525d17 --- /dev/null +++ b/tests/main/refresh/task.yaml @@ -0,0 +1,115 @@ +summary: Check that the refresh command works. +details: | + These tests exercise the refresh command using different store backends. + The concrete store to be used is controlled with the STORE_TYPE variant, + the defined values are fake, for a local store, or remote, for the currently + configured remote store. + When executing against the remote stores the tests rely in the existence of + a given snap with an updatable version (version string like 2.0+fake1) in the + edge channel. + +environment: + SNAP_NAME/strict_fake,strict_remote: test-snapd-tools + SNAP_NAME/classic_fake,classic_remote: test-snapd-classic-confinement + SNAP_VERSION_PATTERN: \d+\.\d+\+fake1 + BLOB_DIR: $(pwd)/fake-store-blobdir + STORE_TYPE/strict_fake,classic_fake: fake + STORE_TYPE/strict_remote,classic_remote: ${REMOTE_STORE} + +prepare: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + fi + + flags= + if [[ $SNAP_NAME =~ classic ]]; then + case "$SPREAD_SYSTEM" in + ubuntu-core-*|fedora-*) + exit + ;; + esac + flags=--classic + fi + + echo "Given a snap is installed" + snap install $flags $SNAP_NAME + + if [ "$STORE_TYPE" = "fake" ]; then + . $TESTSLIB/store.sh + setup_fake_store $BLOB_DIR + + echo "And a new version of that snap put in the controlled store" + . $TESTSLIB/store.sh + init_fake_refreshes $BLOB_DIR $SNAP_NAME + fi + +restore: | + rm -f stderr.out + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/store.sh + teardown_fake_store $BLOB_DIR + fi + +execute: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + fi + + if [[ $SNAP_NAME =~ classic ]]; then + case "$SPREAD_SYSTEM" in + ubuntu-core-*|fedora-*) + exit + ;; + esac + fi + + # FIXME: currently the --list from channel doesn't work + # echo "Then the new version is available for the snap to be refreshed" + # expected="$SNAP_NAME +$SNAP_VERSION_PATTERN" + # snap refresh --list | grep -Pzq "$expected" + # + # echo "=================================" + + echo "When the snap is refreshed" + snap refresh --channel=edge $SNAP_NAME + + echo "Then the new version is listed" + expected="$SNAP_NAME +$SNAP_VERSION_PATTERN" + snap list | grep -Pzq "$expected" + + echo "When a snap is refreshed and has no update it exit 0" + snap refresh $SNAP_NAME 2>stderr.out + cat stderr.out | MATCH "snap \"$SNAP_NAME\" has no updates available" + + echo "classic snaps " + + echo "When multiple snaps have no update we have a good message" + . $TESTSLIB/snaps.sh + install_local basic + snap refresh $SNAP_NAME basic 2>&1 | MATCH "All snaps up to date." + + echo "When moving to stable" + snap refresh --stable $SNAP_NAME + snap info $SNAP_NAME | MATCH "tracking: +stable" + + snap refresh --candidate $SNAP_NAME 2>&1 | MATCH "$SNAP_NAME \(candidate\).*" + snap info $SNAP_NAME | MATCH "tracking: +candidate" diff --git a/tests/main/regression-home-snap-root-owned/task.yaml b/tests/main/regression-home-snap-root-owned/task.yaml new file mode 100644 index 00000000..9da584d1 --- /dev/null +++ b/tests/main/regression-home-snap-root-owned/task.yaml @@ -0,0 +1,34 @@ +summary: Regression test that ensures that $HOME/snap is not root owned for sudo commands + +prepare: | + # ensure we have no snap user data directory yet + rm -rf /home/test/snap + rm -rf /root/snap + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + +execute: | + . $TESTSLIB/dirs.sh + + # run a snap command via sudo + output=$(su -l -c "sudo $SNAP_MOUNT_DIR/bin/test-snapd-tools.env" test) + + # ensure SNAP_USER_DATA points to the right place + echo $output | MATCH SNAP_USER_DATA=/root/snap/test-snapd-tools/x[0-9]+ + echo $output | MATCH HOME=/root/snap/test-snapd-tools/x[0-9]+ + echo $output | MATCH SNAP_USER_COMMON=/root/snap/test-snapd-tools/common + + echo "Verify that the /root/snap directory created and root owned" + if [ $(stat -c '%U' /root/snap) != "root" ]; then + echo "The /root/snap directory is not owned by root" + ls -ld $SNAP_MOUNT_DIR/snap + exit 1 + fi + + echo "Verify that there is no /home/test/snap appearing" + if [ -e /home/test/snap ]; then + user=$(stat -c '%U' /home/test/snap) + echo "An unexpected /home/test/snap directory got created (owner $user)" + ls -ld /home/test/snap + exit 1 + fi diff --git a/tests/main/remove-errors/task.yaml b/tests/main/remove-errors/task.yaml new file mode 100644 index 00000000..c4658ea3 --- /dev/null +++ b/tests/main/remove-errors/task.yaml @@ -0,0 +1,15 @@ +summary: Check remove command errors + +execute: | + echo "Given a core snap is installed" + . "$TESTSLIB/snaps.sh" + install_local test-snapd-tools + + . "$TESTSLIB/names.sh" + echo "Ensure the important snaps can not be removed" + for sn in core $kernel_name $gadget_name; do + if snap remove $sn; then + echo "It should not be possible to remove $sn" + exit 1 + fi + done diff --git a/tests/main/revert-devmode/task.yaml b/tests/main/revert-devmode/task.yaml new file mode 100644 index 00000000..4810463b --- /dev/null +++ b/tests/main/revert-devmode/task.yaml @@ -0,0 +1,83 @@ +summary: Check that revert of a snap in devmode restores devmode + +environment: + STORE_TYPE/fake: fake + STORE_TYPE/remote: ${REMOTE_STORE} + BLOB_DIR: $(pwd)/fake-store-blobdir + +prepare: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + fi + + echo "Given a snap is installed" + snap install --devmode test-snapd-tools + + if [ "$STORE_TYPE" = "fake" ]; then + . $TESTSLIB/store.sh + setup_fake_store $BLOB_DIR + + echo "And a new version of that snap put in the controlled store" + . $TESTSLIB/store.sh + init_fake_refreshes $BLOB_DIR test-snapd-tools + fi + +restore: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/store.sh + teardown_fake_store $BLOB_DIR + fi + +execute: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + fi + + . $TESTSLIB/dirs.sh + + echo "When a refresh is made" + snap refresh --devmode --edge test-snapd-tools + + echo "Then the new version is installed" + snap list | MATCH 'test-snapd-tools +[0-9]+\.[0-9]+\+fake1' + LATEST=$(readlink $SNAP_MOUNT_DIR/test-snapd-tools/current) + + echo "When a revert is made without --devmode flag" + snap revert test-snapd-tools + + echo "Then the old version is active" + snap list | MATCH 'test-snapd-tools +[0-9]+\.[0-9]+ ' + + echo "And the snap runs in devmode" + snap list|MATCH 'test-snapd-tools .* devmode' + + echo "When the latest revision is installed again" + snap remove --revision=$LATEST test-snapd-tools + snap refresh --edge test-snapd-tools + + if [ "$(snap debug confinement)" = strict ] ; then + echo "And revert is made with --jailmode flag" + snap revert --jailmode test-snapd-tools + + echo "Then snap now runs confined (in jailmode, bah)" + snap list|MATCH 'test-snapd-tools .* jailmode' + fi diff --git a/tests/main/revert-sideload/task.yaml b/tests/main/revert-sideload/task.yaml new file mode 100644 index 00000000..24a72599 --- /dev/null +++ b/tests/main/revert-sideload/task.yaml @@ -0,0 +1,20 @@ +summary: Checks for snap sideload reverts + +prepare: | + snap pack $TESTSLIB/snaps/basic + +restore: | + rm -f ./basic_1.0_all.snap + +execute: | + echo Installing sideloaded snap + snap install --dangerous ./basic_1.0_all.snap + snap list | MATCH "x1" + + echo Installing new version of sideloaded snap + snap install --dangerous ./basic_1.0_all.snap + snap list | MATCH "x2" + + echo Reverting to the previous version + snap revert basic | MATCH reverted + snap list | MATCH "x1" diff --git a/tests/main/revert/task.yaml b/tests/main/revert/task.yaml new file mode 100644 index 00000000..7e5a336a --- /dev/null +++ b/tests/main/revert/task.yaml @@ -0,0 +1,100 @@ +summary: Check that revert works. + +environment: + STORE_TYPE/fake: fake + STORE_TYPE/remote: ${REMOTE_STORE} + BLOB_DIR: $(pwd)/fake-store-blobdir + +prepare: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + fi + + echo "Given a snap is installed" + snap install test-snapd-tools + + if [ "$STORE_TYPE" = "fake" ]; then + . $TESTSLIB/store.sh + setup_fake_store $BLOB_DIR + + echo "And a new version of that snap put in the controlled store" + . $TESTSLIB/store.sh + init_fake_refreshes $BLOB_DIR test-snapd-tools + fi + +restore: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/store.sh + teardown_fake_store $BLOB_DIR + fi + +execute: | + if [ "$STORE_TYPE" = "fake" ]; then + if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + exit + fi + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + fi + + echo "Revert without snap name shows error" + if snap revert; then + echo "Reverting without snap name should fail" + exit 1 + fi + + . $TESTSLIB/dirs.sh + + echo "When a refresh is made" + snap refresh --edge test-snapd-tools + + echo "Then the new version is installed" + snap list | MATCH "test-snapd-tools +[0-9]+\.[0-9]+\+fake1" + + echo "And the snap runs" + test-snapd-tools.echo hello|MATCH hello + + echo "When a revert is made" + snap revert test-snapd-tools + + echo "Then the old version is active" + snap list | MATCH "test-snapd-tools +[0-9]+\.[0-9]+ " + + echo "And the data directories are present" + ls $SNAP_MOUNT_DIR/test-snapd-tools | MATCH current + ls /var/snap/test-snapd-tools | MATCH current + + echo "And the snap runs confined" + snap list|MATCH 'test-snapd-tools.* -$' + + echo "And the still snap runs" + test-snapd-tools.echo hello|MATCH hello + + echo "And a new revert fails" + if snap revert test-snapd-tools; then + echo "A revert on an already reverted snap should fail" + exit 1 + fi + + echo "And a refresh doesn't update the snap" + snap refresh + snap list | MATCH "test-snapd-tools +[0-9]+\.[0-9]+ " + + echo "Unless the snap is asked for explicitly" + snap refresh --edge test-snapd-tools + snap list | MATCH "test-snapd-tools +[0-9]+\.[0-9]+\+fake1" diff --git a/tests/main/searching/task.yaml b/tests/main/searching/task.yaml new file mode 100644 index 00000000..768692b0 --- /dev/null +++ b/tests/main/searching/task.yaml @@ -0,0 +1,41 @@ +summary: Check snap search + +execute: | + echo "List all featured snaps" + expected="(?s)Name +Version +Developer +Notes +Summary *\n\ + (.*?\n)?\ + .*" + snap find | grep -Pzq "$expected" + if [ $(snap find | wc -l) -gt 50 ]; then + echo "Found more than 50 featured apps, this seems bogus:" + snap find + exit 1 + fi + if [ $(snap find | wc -l) -lt 2 ]; then + echo "Not found any featured app, this seems bogus:" + snap find + exit 1 + fi + + echo "Exact matches" + for snapName in test-snapd-tools test-snapd-python-webserver + do + expected="(?s)Name +Version +Developer +Notes +Summary *\n\ + (.*?\n)?\ + $snapName +.*? *\n\ + .*" + snap find $snapName | grep -Pzq "$expected" + done + + echo "Partial terms work too" + expected="(?s)Name +Version +Developer +Notes +Summary *\n\ + (.*?\n)?\ + test-snapd-tools +.*? *\n\ + .*" + snap find test-snapd- | grep -Pzq "$expected" + + # cassandra only available for amd64 + if [ $(uname -m) = "x86_64" ]; then + echo "List of snaps in a section works" + snap find --section=database | MATCH cassandra + fi diff --git a/tests/main/security-apparmor/task.yaml b/tests/main/security-apparmor/task.yaml new file mode 100644 index 00000000..b2082aa1 --- /dev/null +++ b/tests/main/security-apparmor/task.yaml @@ -0,0 +1,22 @@ +summary: Check basic apparmor confinement rules. + +prepare: | + echo "Given a basic snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools +execute: | + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + echo "Then an unconfined action should succeed" + test-snapd-tools.cmd touch /dev/shm/snap.test-snapd-tools.foo + test -f /dev/shm/snap.test-snapd-tools.foo + + echo "Then a confined action should fail" + if test-snapd-tools.cmd touch /dev/shm/snap.not-test-snapd-tools.foo 2>touch.error; then + echo "Expected error" + exit 1 + fi + [ "$(cat touch.error)" = "touch: cannot touch '/dev/shm/snap.not-test-snapd-tools.foo': Permission denied" ] +restore: | + rm -f touch.error diff --git a/tests/main/security-device-cgroups-classic/task.yaml b/tests/main/security-device-cgroups-classic/task.yaml new file mode 100644 index 00000000..997e8a77 --- /dev/null +++ b/tests/main/security-device-cgroups-classic/task.yaml @@ -0,0 +1,37 @@ +summary: Check that device nodes are available in classic + +details: | + This tests that a framebuffer device is accessible in classic and makes + sure that other devices are still accessible (ie, the cgroup is not in + effect). + +# Disabled on Fedora and Ubuntu Core because they don't support classic +# confinement. +systems: [-fedora-*, -ubuntu-core-*] + +prepare: | + # Create framebuffer device node and give it some content we can verify + # the test snap can read. + if [ ! -e /dev/fb0 ]; then + mknod /dev/fb0 c 29 0 + touch /dev/fb0.spread + fi + + echo "Given a snap declaring a plug on framebuffer is installed in classic" + . $TESTSLIB/snaps.sh + install_local_classic test-classic-cgroup + +restore: | + if [ -e /dev/fb0.spread ]; then + rm -f /dev/fb0 /dev/fb0.spread + fi + +execute: | + . $TESTSLIB/dirs.sh + + # classic snaps don't use 'plugs', so just test the accesses after install + echo "the classic snap can access the framebuffer" + "$SNAP_MOUNT_DIR"/bin/test-classic-cgroup.read-fb 2>&1 | MATCH -v '(Permission denied|Operation not permitted)' + + echo "the classic snap can access other devices" + test "`$SNAP_MOUNT_DIR/bin/test-classic-cgroup.read-kmsg`" diff --git a/tests/main/security-device-cgroups-devmode/task.yaml b/tests/main/security-device-cgroups-devmode/task.yaml new file mode 100644 index 00000000..0259c4ee --- /dev/null +++ b/tests/main/security-device-cgroups-devmode/task.yaml @@ -0,0 +1,42 @@ +summary: Check that plugged and unplugged device nodes are available in devmode + +details: | + This tests that a framebuffer device is accessible in devmode and makes + sure that other devices not included in the snap's plugged interfaces are + still accessible (ie, the cgroup is not in effect). + +prepare: | + # Create framebuffer device node and give it some content we can verify + # the test snap can read. + if [ ! -e /dev/fb0 ]; then + mknod /dev/fb0 c 29 0 + touch /dev/fb0.spread + fi + + echo "Given a snap declaring a plug on framebuffer is installed in devmode" + . $TESTSLIB/snaps.sh + install_local_devmode test-devmode-cgroup + +restore: | + if [ -e /dev/fb0.spread ]; then + rm -f /dev/fb0 /dev/fb0.spread + fi + +execute: | + . $TESTSLIB/dirs.sh + + echo "And the framebuffer plug is connected" + snap connect test-devmode-cgroup:framebuffer + echo "the devmode snap can access the framebuffer" + "$SNAP_MOUNT_DIR"/bin/test-devmode-cgroup.read-fb 2>&1 | MATCH -v '(Permission denied|Operation not permitted)' + + echo "the devmode snap can access other devices" + test "`$SNAP_MOUNT_DIR/bin/test-devmode-cgroup.read-kmsg`" + + echo "And the framebuffer plug is disconnected" + snap disconnect test-devmode-cgroup:framebuffer + echo "the devmode snap can access the framebuffer" + "$SNAP_MOUNT_DIR"/bin/test-devmode-cgroup.read-fb 2>&1 | MATCH -v '(Permission denied|Operation not permitted)' + + echo "the devmode snap can access other devices" + test "`$SNAP_MOUNT_DIR/bin/test-devmode-cgroup.read-kmsg`" diff --git a/tests/main/security-device-cgroups-jailmode/task.yaml b/tests/main/security-device-cgroups-jailmode/task.yaml new file mode 100644 index 00000000..7ae930f8 --- /dev/null +++ b/tests/main/security-device-cgroups-jailmode/task.yaml @@ -0,0 +1,45 @@ +summary: Check that plugged and unplugged device nodes are available in jailmode + +details: | + This tests that a framebuffer device is accessible in jailmode and makes + sure that other devices not included in the snap's plugged interfaces are + still accessible (ie, the cgroup is not in effect). + +# None of those systems support strict confinement which is required to formally enable jailmode. +systems: [-fedora-*, -opensuse-*, -debian-*] + +prepare: | + # Create framebuffer device node and give it some content we can verify + # the test snap can read. + if [ ! -e /dev/fb0 ]; then + mknod /dev/fb0 c 29 0 + touch /dev/fb0.spread + fi + + echo "Given a snap declaring a plug on framebuffer is installed in jailmode" + . $TESTSLIB/snaps.sh + install_local_jailmode test-devmode-cgroup + +restore: | + if [ -e /dev/fb0.spread ]; then + rm -f /dev/fb0 /dev/fb0.spread + fi + +execute: | + . $TESTSLIB/dirs.sh + + echo "And the framebuffer plug is connected" + snap connect test-devmode-cgroup:framebuffer + echo "the jailmode snap can access the framebuffer" + "$SNAP_MOUNT_DIR"/bin/test-devmode-cgroup.read-fb 2>&1 | MATCH -v '(Permission denied|Operation not permitted)' + + echo "the jailmode snap cannot access other devices" + "$SNAP_MOUNT_DIR"/bin/test-devmode-cgroup.read-kmsg 2>&1 | MATCH '(Permission denied|Operation not permitted)' + + echo "And the framebuffer plug is disconnected" + snap disconnect test-devmode-cgroup:framebuffer + echo "the jailmode snap cannot access the framebuffer" + "$SNAP_MOUNT_DIR"/bin/test-devmode-cgroup.read-fb 2>&1 | MATCH '(Permission denied|Operation not permitted)' + + echo "the jailmode snap cannot access other devices" + "$SNAP_MOUNT_DIR"/bin/test-devmode-cgroup.read-kmsg 2>&1 | MATCH '(Permission denied|Operation not permitted)' diff --git a/tests/main/security-device-cgroups-serial-port/task.yaml b/tests/main/security-device-cgroups-serial-port/task.yaml new file mode 100644 index 00000000..d1b9cddc --- /dev/null +++ b/tests/main/security-device-cgroups-serial-port/task.yaml @@ -0,0 +1,55 @@ +summary: Ensure that the device cgroup works properly for serial-port. + +# We don't run the native kernel on these distributions yet so we can't +# load kernel modules coming from distribution packages yet. +systems: [-fedora-*, -opensuse-*, -debian-*] + +prepare: | + # create serial devices if they don't exist + if [ ! -e /dev/ttyS4 ]; then + mknod /dev/ttyS4 c 4 68 + touch /dev/ttyS4.spread + fi + +restore: | + if [ -e /dev/ttyS4.spread ]; then + rm -f /dev/ttyS4 /dev/ttyS4.spread + fi + + udevadm control --reload-rules + udevadm trigger + +execute: | + echo "Given a snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + + echo "Then the device is not assigned to that snap" + ! udevadm info /dev/ttyS4 | MATCH "E: TAGS=.*snap_test-snapd-tools_env" + + echo "And the device is not shown in the snap device list" + # FIXME: this is, apparently, a layered can of worms. Zyga says he needs to fix it. + if [ -e /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list ]; then + MATCH -v "c 4:68 rwm" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + fi + + echo "=================================================" + + echo "When a udev rule assigning the device to the snap is added" + content="SUBSYSTEM==\"tty\", KERNEL==\"ttyS4\", TAG+=\"snap_test-snapd-tools_env\"" + echo "$content" > /etc/udev/rules.d/70-snap.test-snapd-tools.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + + echo "Then the device is shown as assigned to the snap" + udevadm info /dev/ttyS4 | MATCH "E: TAGS=.*snap_test-snapd-tools_env" + + echo "=================================================" + + echo "When a snap command is called" + test-snapd-tools.env + + echo "Then the device is shown in the snap device list" + MATCH "c 4:68 rwm" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list diff --git a/tests/main/security-device-cgroups-strict/task.yaml b/tests/main/security-device-cgroups-strict/task.yaml new file mode 100644 index 00000000..ea1a1ac5 --- /dev/null +++ b/tests/main/security-device-cgroups-strict/task.yaml @@ -0,0 +1,44 @@ +summary: Check that plugged and unplugged device nodes are available in strict + +details: | + This tests that a framebuffer device is accessible in strict and makes + sure that other devices not included in the snap's plugged interfaces are + still accessible (ie, the cgroup is not in effect). + +systems: [-fedora-*, -opensuse-*,-debian-*] + +prepare: | + # Create framebuffer device node and give it some content we can verify + # the test snap can read. + if [ ! -e /dev/fb0 ]; then + mknod /dev/fb0 c 29 0 + touch /dev/fb0.spread + fi + + echo "Given a snap declaring a plug on framebuffer is installed in strict" + . $TESTSLIB/snaps.sh + install_local test-strict-cgroup + +restore: | + if [ -e /dev/fb0.spread ]; then + rm -f /dev/fb0 /dev/fb0.spread + fi + +execute: | + . $TESTSLIB/dirs.sh + + echo "And the framebuffer plug is connected" + snap connect test-strict-cgroup:framebuffer + echo "the strict snap can access the framebuffer" + "$SNAP_MOUNT_DIR"/bin/test-strict-cgroup.read-fb 2>&1 | MATCH -v '(Permission denied|Operation not permitted)' + + echo "the strict snap cannot access other devices" + "$SNAP_MOUNT_DIR"/bin/test-strict-cgroup.read-kmsg 2>&1 | MATCH '(Permission denied|Operation not permitted)' + + echo "And the framebuffer plug is disconnected" + snap disconnect test-strict-cgroup:framebuffer + echo "the strict snap cannot access the framebuffer" + "$SNAP_MOUNT_DIR"/bin/test-strict-cgroup.read-fb 2>&1 | MATCH '(Permission denied|Operation not permitted)' + + echo "the strict snap cannot access other devices" + "$SNAP_MOUNT_DIR"/bin/test-strict-cgroup.read-kmsg 2>&1 | MATCH '(Permission denied|Operation not permitted)' diff --git a/tests/main/security-device-cgroups/task.yaml b/tests/main/security-device-cgroups/task.yaml new file mode 100644 index 00000000..f6ea706b --- /dev/null +++ b/tests/main/security-device-cgroups/task.yaml @@ -0,0 +1,121 @@ +summary: Ensure that the security rules related to device cgroups work. + +# We don't run the native kernel on these distributions yet so we can't +# load kernel modules coming from distribution packages yet. +systems: [-fedora-*, -opensuse-*] + +environment: + DEVICE_NAME/kmsg: kmsg + UDEVADM_PATH/kmsg: /sys/devices/virtual/mem/kmsg + DEVICE_ID/kmsg: "c 1:11 rwm" + OTHER_DEVICE_NAME/kmsg: uinput + OTHER_UDEVADM_PATH/kmsg: /sys/devices/virtual/misc/uinput + OTHER_DEVICE_ID/kmsg: "c 10:223 rwm" + + DEVICE_NAME/uinput: uinput + UDEVADM_PATH/uinput: /sys/devices/virtual/misc/uinput + DEVICE_ID/uinput: "c 10:223 rwm" + OTHER_DEVICE_NAME/uinput: kmsg + OTHER_UDEVADM_PATH/uinput: /sys/devices/virtual/mem/kmsg + OTHER_DEVICE_ID/uinput: "c 1:11 rwm" + +prepare: | + if [ ! -e /sys/devices/virtual/misc/uinput ]; then + modprobe uinput + fi + # create nvidia devices if they don't exist + if [ ! -e /dev/nvidia0 ]; then + mknod /dev/nvidia0 c 195 0 + touch /dev/nvidia0.spread + fi + if [ ! -e /dev/nvidiactl ]; then + mknod /dev/nvidiactl c 195 255 + touch /dev/nvidiactl.spread + fi + if [ ! -e /dev/nvidia-uvm ]; then + mknod /dev/nvidia-uvm c 247 0 + touch /dev/nvidia-uvm.spread + fi + # move aside an existing nvidia device + if [ -e /dev/nvidia254 ]; then + mv /dev/nvidia254 /dev/nvidia254.spread + fi + # create uhid device if it doesn't exist + if [ ! -e /dev/uhid ]; then + mknod /dev/uhid c 10 239 + touch /dev/uhid.spread + fi + +restore: | + if [ -e /dev/nvidia0.spread ]; then + rm -f /dev/nvidia0 /dev/nvidia0.spread + fi + if [ -e /dev/nvidiactl.spread ]; then + rm -f /dev/nvidiactl /dev/nvidiactl.spread + fi + if [ -e /dev/nvidia-uvm.spread ]; then + rm -f /dev/nvidia-uvm /dev/nvidia-uvm.spread + fi + if [ -e /dev/nvidia254.spread ]; then + mv /dev/nvidia254.spread /dev/nvidia254 + fi + if [ -e /dev/uhid.spread ]; then + rm -f /dev/uhid /dev/uhid.spread + fi + rm -f /etc/udev/rules.d/70-snap.test-snapd-tools.rules + udevadm control --reload-rules + udevadm trigger + +execute: | + echo "Given a snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + + echo "Then the device is not assigned to that snap" + ! udevadm info $UDEVADM_PATH | MATCH "E: TAGS=.*snap_test-snapd-tools_env" + + echo "And the device is not shown in the snap device list" + # FIXME: this is, apparently, a layered can of worms. Zyga says he needs to fix it. + if [ -e /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list ]; then + MATCH -v "$DEVICE_ID" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + fi + + echo "=================================================" + + echo "When a udev rule assigning the device to the snap is added" + content="KERNEL==\"$DEVICE_NAME\", TAG+=\"snap_test-snapd-tools_env\"" + echo "$content" > /etc/udev/rules.d/70-snap.test-snapd-tools.rules + udevadm control --reload-rules + udevadm settle + udevadm trigger + udevadm settle + + echo "Then the device is shown as assigned to the snap" + udevadm info $UDEVADM_PATH | MATCH "E: TAGS=.*snap_test-snapd-tools_env" + + echo "And other devices are not shown as assigned to the snap" + udevadm info $OTHER_UDEVADM_PATH | MATCH -v "E: TAGS=.*snap_test-snapd-tools_env" + + echo "=================================================" + + echo "When a snap command is called" + test-snapd-tools.env + + echo "Then the device is shown in the snap device list" + MATCH "$DEVICE_ID" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + + echo "And other devices are not shown in the snap device list" + MATCH -v "$OTHER_DEVICE_ID" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + + echo "But existing nvidia devices are in the snap's device cgroup" + MATCH "c 195:0 rwm" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + MATCH "c 195:255 rwm" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + MATCH "c 247:0 rwm" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + + echo "But nonexisting nvidia devices are not" + MATCH -v "c 195:254 rwm" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + + echo "But the existing uhid device is in the snap's device cgroup" + MATCH "c 10:239 rwm" < /sys/fs/cgroup/devices/snap.test-snapd-tools.env/devices.list + + # TODO: check device unassociated after removing the udev file and rebooting diff --git a/tests/main/security-devpts/task.yaml b/tests/main/security-devpts/task.yaml new file mode 100644 index 00000000..e93e76e8 --- /dev/null +++ b/tests/main/security-devpts/task.yaml @@ -0,0 +1,35 @@ +summary: Ensure that the basic devpts security rules are in place. + +execute: | + if [ "$(snap debug confinement)" = none ] ; then + exit 0 + fi + + echo "Given a basic snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-devpts + + CONNECTED_PATTERN=":physical-memory-observe +test-snapd-devpts" + DISCONNECTED_PATTERN="\- +test-snapd-devpts:physical-memory-observe" + + echo "When no plugs are not connected" + if snap interfaces | MATCH "$CONNECTED_PATTERN" ; then + snap disconnect test-snapd-devpts:physical-memory-observe + snap interfaces | MATCH "$DISCONNECTED_PATTERN" + fi + + echo "Then can openpty" + test-snapd-devpts.openpty | MATCH PASS + + echo "Then can access slave PTY" + test-snapd-devpts.useptmx | MATCH PASS + + echo "When a udev tagging plug is connected" + snap connect test-snapd-devpts:physical-memory-observe + snap interfaces | MATCH "$CONNECTED_PATTERN" + + echo "Then can openpty" + test-snapd-devpts.openpty | MATCH PASS + + echo "Then can access slave PTY" + test-snapd-devpts.useptmx | MATCH PASS diff --git a/tests/main/security-private-tmp/task.yaml b/tests/main/security-private-tmp/task.yaml new file mode 100644 index 00000000..b56c6254 --- /dev/null +++ b/tests/main/security-private-tmp/task.yaml @@ -0,0 +1,49 @@ +summary: Ensure that the security rules for private tmp are in place. + +# ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] + +environment: + SNAP_INSTALL_DIR: $(pwd)/snap-install-dir + +prepare: | + echo "Given a basic snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + + echo "And another basic snap is installed" + mkdir -p $SNAP_INSTALL_DIR + cp -ra $TESTSLIB/snaps/test-snapd-tools/* $SNAP_INSTALL_DIR + sed -i 's/test-snapd-tools/not-test-snapd-tools/g' $SNAP_INSTALL_DIR/meta/snap.yaml + snap pack $SNAP_INSTALL_DIR + snap install --dangerous not-test-snapd-tools_1.0_all.snap + +restore: | + rm -rf not-test-snapd-tools_1.0_all.snap "$SNAP_INSTALL_DIR" /tmp/foo *stat.error + +execute: | + . $TESTSLIB/dirs.sh + + echo "When a temporary file is created by one snap" + expect -d -f tmp-create.exp + + if [ -e "$LIBEXECDIR/snapd/snap-discard-ns" ]; then + echo "Then that file is accessible from other calls of commands from the same snap" + if ! test-snapd-tools.cmd stat /tmp/foo 2>same-stat.error; then + echo "Expected the file to be present" + exit 1 + fi + else + echo "Then that file is not accessible from other calls of commands from the same snap" + if test-snapd-tools.cmd stat /tmp/foo 2>same-stat.error; then + echo "Expected the file to be absent" + exit 1 + fi + fi + + echo "And that file is not accessible by other snaps" + if not-test-snapd-tools.cmd stat /tmp/foo 2>other-stat.error; then + echo "Expected error not present" + exit 1 + fi + MATCH "stat: cannot stat '/tmp/foo': No such file or directory" < other-stat.error diff --git a/tests/main/security-private-tmp/tmp-create.exp b/tests/main/security-private-tmp/tmp-create.exp new file mode 100644 index 00000000..98201914 --- /dev/null +++ b/tests/main/security-private-tmp/tmp-create.exp @@ -0,0 +1,15 @@ +#!/usr/bin/expect -f + +set timeout 20 + +spawn bash + +# Test private /tmp, allowed access +spawn su -l -c "$env(SNAP_MOUNT_DIR)/bin/test-snapd-tools.sh" test +expect "bash-4.3\\$" {} timeout { exit 1 } +send "touch /tmp/foo\n" +send "stat /tmp/foo\n" +expect { + timeout { exit 1 } + "File: '/tmp/foo'*Size: 0" +} diff --git a/tests/main/security-profiles/task.yaml b/tests/main/security-profiles/task.yaml new file mode 100644 index 00000000..654b06c2 --- /dev/null +++ b/tests/main/security-profiles/task.yaml @@ -0,0 +1,31 @@ +summary: Check security profile generation for apps and hooks. + +prepare: | + snap pack $TESTSLIB/snaps/basic-hooks +restore: | + rm -f basic-hooks_1.0_all.snap + +execute: | + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + seccomp_profile_directory="/var/lib/snapd/seccomp/bpf" + + echo "Security profiles are generated and loaded for apps" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles) + + for profile in snap.test-snapd-tools.block snap.test-snapd-tools.cat snap.test-snapd-tools.echo snap.test-snapd-tools.fail snap.test-snapd-tools.success + do + MATCH "^${profile} \(enforce\)$" <<<"$loaded_profiles" + [ -f "$seccomp_profile_directory/${profile}.bin" ] + done + + echo "Security profiles are generated and loaded for hooks" + snap install --dangerous basic-hooks_1.0_all.snap + loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles) + + echo "$loaded_profiles" | MATCH "^snap.basic-hooks.hook.configure \(enforce\)$" + [ -f "$seccomp_profile_directory/snap.basic-hooks.hook.configure.bin" ] diff --git a/tests/main/security-setuid-root/task.yaml b/tests/main/security-setuid-root/task.yaml new file mode 100644 index 00000000..27daf6c4 --- /dev/null +++ b/tests/main/security-setuid-root/task.yaml @@ -0,0 +1,41 @@ +summary: Check that snap-confine refuses to run unconfined + +systems: + # No confinement (AppArmor, Seccomp) available on these systems + - -debian-* + - -fedora-* + - -opensuse-* + +details: | + snap-confine is setuid root but is only confined with an apparmor profile + when invoked as a system-installed package or when running in the core from + the usual location (/usr/lib/snapd/snap-confine). As a security precaution + it should detect and refuse to run if invoked from the core snap. +prepare: | + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + echo "Ensure the snap-confine profiles on core are not loaded" + for p in /etc/apparmor.d/snap.core.*.usr.lib.snapd.snap-confine; do + apparmor_parser -R "$p" + done +restore: | + echo "Ensure the snap-confine profiles are restored" + for p in /etc/apparmor.d/snap.core.*.usr.lib.snapd.snap-confine; do + apparmor_parser -r $p + done +execute: | + . $TESTSLIB/dirs.sh + + # NOTE: This has to run as the test user because the protection is only + # active if user gains elevated permissions as a result of using setuid + # root snap-confine. + if su test -c "sh -c \"SNAP_NAME=test-snapd-tools $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snap-confine snap.test-snapd-tools.cmd /bin/true 2>/dev/null\""; then + echo "snap-confine didn't refuse to run!" + exit 1 + fi + su test -c "sh -c \"SNAP_NAME=test-snapd-tools $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snap-confine snap.test-snapd-tools.cmd /bin/true 2>&1\"" | MATCH "Refusing to continue to avoid permission escalation attacks" +debug: | + ls -ld $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snap-confine || true + ls -ld $SNAP_MOUNT_DIR/ubuntu-core/current/usr/lib/snapd/snap-confine || true + ls -ld /usr/lib/snapd/snap-confine || true + snap list diff --git a/tests/main/server-snap/task.yaml b/tests/main/server-snap/task.yaml new file mode 100644 index 00000000..0be6e906 --- /dev/null +++ b/tests/main/server-snap/task.yaml @@ -0,0 +1,36 @@ +summary: Check snap web servers + +systems: [-fedora-*, -opensuse-*] + +environment: + SNAP_NAME/pythonServer: test-snapd-python-webserver + IP_VERSION/pythonServer: 4 + PORT/pythonServer: 80 + TEXT/pythonServer: XKCD rocks! + LOCALHOST/pythonServer: localhost + SNAP_NAME/goServer: test-snapd-go-webserver + IP_VERSION/goServer: 6 + PORT/goServer: 8081 + TEXT/goServer: Hello World + LOCALHOST/goServer: ip6-localhost + +warn-timeout: 3m + +prepare: | + snap install $SNAP_NAME + cat > request.txt <fake.store + echo >>fake.store + cat $TESTSLIB/assertions/developer1.account >>fake.store + echo >>fake.store + cat $TESTSLIB/assertions/fake.store >>fake.store + echo "Ack fake store assertion" + snap ack fake.store + + echo "And a new version of that snap put in the controlled store" + init_fake_refreshes $BLOB_DIR $SNAP_NAME + +restore: | + rm -f fake.store + rm -f stderr.out + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + snap set core proxy.store= + + . $TESTSLIB/store.sh + teardown_fake_store $BLOB_DIR + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + echo "Configure to use the fakestore through store assertion and proxy.store" + snap set core proxy.store=fake + + echo "Then the new version is listed as candidate refresh" + expected="$SNAP_NAME +$SNAP_VERSION_PATTERN" + snap refresh --list | grep -Pzq "$expected" + + echo "Switch back temporarely to the main store" + snap set core proxy.store= + ! snap refresh --list | grep -Pzq "$expected" + + echo "Configure back to use fakestore" + snap set core proxy.store=fake + + echo "Now we can proceed with the refresh from the fakestore" + snap refresh $SNAP_NAME + + echo "Then the new version is listed" + snap list | grep -Pzq "$expected" diff --git a/tests/main/snap-auto-import-asserts-spools/task.yaml b/tests/main/snap-auto-import-asserts-spools/task.yaml new file mode 100644 index 00000000..55da2aec --- /dev/null +++ b/tests/main/snap-auto-import-asserts-spools/task.yaml @@ -0,0 +1,54 @@ +summary: Check that `snap auto-import` works as expected + +systems: [ubuntu-core-16-64] + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "Ensure the testrootorg-store.account-key is not already added" + output=$(snap known account-key | grep -c "name: test-store" || true) + if [ "$output" != "0" ]; then + echo " testrootorg-store.account-key is already added" + exit 1 + fi + echo "Create a ramdisk with the testrootorg-store.account-key assertion" + . "$TESTSLIB/ramdisk.sh" + setup_ramdisk + mkfs.vfat /dev/ram0 + mount /dev/ram0 /mnt + cp $TESTSLIB/assertions/testrootorg-store.account-key /mnt/auto-import.assert + sync + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + rm -rf /var/lib/snapd/auto-import/* + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "Simulate a not running snapd (happens on e.g. early boot)" + systemctl stop snapd.service snapd.socket + + echo "`snap auto-import` spooled assertions if it can not talk to snapd" + snap auto-import + ls /run/snapd/auto-import + umount /mnt + systemctl start snapd.service snapd.socket + + echo "`snap auto-import` reads from the auto-import dir" + snap auto-import + snap known account-key | MATCH "name: test-store" + + nr=$(ls /run/snapd/auto-import|wc -l) + if [ "$nr" != "0" ]; then + echo "Expected an empty /run/snapd/auto-import got:" + ls /run/snapd/auto-import + exit 1 + fi diff --git a/tests/main/snap-auto-import-asserts/task.yaml b/tests/main/snap-auto-import-asserts/task.yaml new file mode 100644 index 00000000..786d458e --- /dev/null +++ b/tests/main/snap-auto-import-asserts/task.yaml @@ -0,0 +1,38 @@ +summary: Check that `snap auto-import` works as expected + +systems: [ubuntu-core-16-64] + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "Ensure the testrootorg-store.account-key is not already added" + output=$(snap known account-key | grep -c "name: test-store" || true) + if [ "$output" != "0" ]; then + echo " testrootorg-store.account-key is already added" + exit 1 + fi + echo "Create a ramdisk with the testrootorg-store.account-key assertion" + . "$TESTSLIB/ramdisk.sh" + setup_ramdisk + mkfs.vfat /dev/ram0 + mount /dev/ram0 /mnt + cp $TESTSLIB/assertions/testrootorg-store.account-key /mnt/auto-import.assert + sync + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + umount /mnt + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "`snap auto-import` imports assertions from the mounted ramdisk" + snap auto-import + snap known account-key | MATCH "name: test-store" diff --git a/tests/main/snap-auto-mount/task.yaml b/tests/main/snap-auto-mount/task.yaml new file mode 100644 index 00000000..fe91f20e --- /dev/null +++ b/tests/main/snap-auto-mount/task.yaml @@ -0,0 +1,56 @@ +summary: Check that `snap auto-import` works as expected + +systems: [ubuntu-core-16-64] + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "Install dmsetup" + snap install --devmode --edge dmsetup + + echo "Ensure the testrootorg-store.account-key is not already added" + output=$(snap known account-key | grep -c "name: test-store" || true) + if [ "$output" != "0" ]; then + echo " testrootorg-store.account-key is already added" + exit 1 + fi + echo "Create a ramdisk with the testrootorg-store.account-key assertion" + . "$TESTSLIB/ramdisk.sh" + setup_ramdisk + mkfs.vfat /dev/ram0 + mount /dev/ram0 /mnt + cp $TESTSLIB/assertions/testrootorg-store.account-key /mnt/auto-import.assert + sync + umount /mnt + echo "Create new block device to trigger auto-import mount" + # wait for all udev events to be handled, sometimes we are getting an error: + # + # $ dmsetup -v --noudevsync --noudevrules create dm-ram0 --table '0 131072 linear /dev/ram0 0' + # device-mapper: reload ioctl on dm-ram0 failed: Device or resource busy + # + # and in syslog: + # + # Jun 28 09:18:34 localhost kernel: [ 36.434220] device-mapper: table: 252:0: linear: Device lookup failed + # Jun 28 09:18:34 localhost kernel: [ 36.434686] device-mapper: ioctl: error adding target to table + udevadm settle + dmsetup -v --noudevsync --noudevrules create dm-ram0 --table "0 $(blockdev --getsize /dev/ram0) linear /dev/ram0 0" + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + dmsetup -v --noudevsync --noudevrules remove dm-ram0 + +debug: | + tail -n 20 /var/log/syslog + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "The auto-mount magic has given us the assertion" + snap known account-key | MATCH "name: test-store" diff --git a/tests/main/snap-confine-from-core/task.yaml b/tests/main/snap-confine-from-core/task.yaml new file mode 100644 index 00000000..ae41b165 --- /dev/null +++ b/tests/main/snap-confine-from-core/task.yaml @@ -0,0 +1,28 @@ +summary: Test that snap-confine is run from core on re-exec + +# Disable for Fedora, openSUSE as re-exec is not support there yet +systems: [-ubuntu-core-16-*, -fedora-*, -opensuse-*] + +prepare: | + echo "Installing test-snapd-tools" + snap install test-snapd-tools + echo "Breaking host snap-confine" + chmod 0755 /usr/lib/snapd/snap-confine + +restore: | + echo "Restoring host snap-confine" + chmod 4755 /usr/lib/snapd/snap-confine + +execute: | + if [ "${SNAP_REEXEC:-}" = "0" ]; then + echo "skipping test when SNAP_REEXEC is disabled" + exit 0 + fi + + echo "Ensure we re-exec by default" + snap list + journalctl | MATCH "DEBUG: restarting into" + + echo "Ensure snap-confine from the core snap is run" + # do not use "strace -f" for unknown reasons that hangs + test-snapd-tools.echo hello diff --git a/tests/main/snap-confine-privs/task.yaml b/tests/main/snap-confine-privs/task.yaml new file mode 100644 index 00000000..427602bb --- /dev/null +++ b/tests/main/snap-confine-privs/task.yaml @@ -0,0 +1,63 @@ +summary: ensure that snap-confine handles group and user privileges correctly +details: | + The openSUSE security team has made a remark about a particular part of + snap-confine's UID/GID handling. The code there was correct but this test + is here to demonstrate that and ensure it never regresses. + + Security review https://bugzilla.opensuse.org/show_bug.cgi?id=986050 +# This test is not executed on a core system simply because of the hassle of +# building the support C program. In the future it might be improved with the +# use of the classic snap where we just use classic to build the helper. +systems: [-ubuntu-core-16-*] +environment: + # This is used to abbreviate some of the paths below. + P: /var/snap/test-snapd-sh/common +prepare: | + echo "Install a helper snap (for confinement testing)" + . $TESTSLIB/snaps.sh + install_local test-snapd-sh + + echo "Compile and prepare the support program" + # Because we use the snap data directory we don't need to clean it up + # manually as all snaps and their data are reset after each test. + gcc -Wall -Wextra -Werror ./uids-and-gids.c -o $P/uids-and-gids + cp $P/uids-and-gids $P/uids-and-gids-setuid + chown root $P/uids-and-gids-setuid + chmod 4755 $P/uids-and-gids-setuid + cp $P/uids-and-gids $P/uids-and-gids-setgid + chgrp root $P/uids-and-gids-setgid + chmod 2755 $P/uids-and-gids-setgid +execute: | + echo "The test executables files have the expected mode and ownership" + ls -l $P | MATCH -- '-rwxr-xr-x 1 root root [0-9]+ [A-Z][a-z]+ +[0-9]+ [0-9]+:[0-9]+ uids-and-gids' + ls -l $P | MATCH -- '-rwxr-sr-x 1 root root [0-9]+ [A-Z][a-z]+ +[0-9]+ [0-9]+:[0-9]+ uids-and-gids-setgid' + ls -l $P | MATCH -- '-rwsr-xr-x 1 root root [0-9]+ [A-Z][a-z]+ +[0-9]+ [0-9]+:[0-9]+ uids-and-gids-setuid' + + echo "Running as regular user" + # Spread runs all tests as root so we're using su to switch to the "test" user. + # The "test" user inside the spread suite is guaranteed to have UID/GID of 12345. + su -l -c $P/uids-and-gids test | MATCH 'ruid=12345 euid=12345 suid=12345 rgid=12345 egid=12345 sgid=12345' + su -l -c $P/uids-and-gids-setuid test | MATCH 'ruid=12345 euid=0 suid=0 rgid=12345 egid=12345 sgid=12345' + su -l -c $P/uids-and-gids-setgid test | MATCH 'ruid=12345 euid=12345 suid=12345 rgid=12345 egid=0 sgid=0 ' + + echo "Running as regular user via sudo" + # This is same as above except that we're also using sudo + su -l -c "sudo $P/uids-and-gids" test | MATCH 'ruid=0 euid=0 suid=0 rgid=0 egid=0 sgid=0 ' + su -l -c "sudo $P/uids-and-gids-setuid" test | MATCH 'ruid=0 euid=0 suid=0 rgid=0 egid=0 sgid=0 ' + su -l -c "sudo $P/uids-and-gids-setgid" test | MATCH 'ruid=0 euid=0 suid=0 rgid=0 egid=0 sgid=0 ' + + echo "Running as regular user under snap-confine" + # This is the same as the two above but it goes through snap-confine as + # well. Note that we have to quote the $ sign below as there are two shell + # expansions done. Note that we are using "snap run test-snapd-sh" in order + # to ensure that we can start the progam even if su/sudo's secure PATH does + # not contain the snap bin directory. + su -l -c "snap run test-snapd-sh -c '\$SNAP_COMMON/uids-and-gids'" test | MATCH 'ruid=12345 euid=12345 suid=12345 rgid=12345 egid=12345 sgid=12345' + su -l -c "snap run test-snapd-sh -c '\$SNAP_COMMON/uids-and-gids-setuid'" test | MATCH 'ruid=12345 euid=0 suid=0 rgid=12345 egid=12345 sgid=12345' + su -l -c "snap run test-snapd-sh -c '\$SNAP_COMMON/uids-and-gids-setgid'" test | MATCH 'ruid=12345 euid=12345 suid=12345 rgid=12345 egid=0 sgid=0 ' + + echo "Running as regular user, uder snap-conifne under sudo" + # This is the same one as the previous one but also using sudo. + su -l -c "sudo snap run test-snapd-sh -c '\$SNAP_COMMON/uids-and-gids'" test | MATCH 'ruid=0 euid=0 suid=0 rgid=0 egid=0 sgid=0 ' + su -l -c "sudo snap run test-snapd-sh -c '\$SNAP_COMMON/uids-and-gids-setuid'" test | MATCH 'ruid=0 euid=0 suid=0 rgid=0 egid=0 sgid=0 ' + su -l -c "sudo snap run test-snapd-sh -c '\$SNAP_COMMON/uids-and-gids-setgid'" test | MATCH 'ruid=0 euid=0 suid=0 rgid=0 egid=0 sgid=0 ' diff --git a/tests/main/snap-confine-privs/uids-and-gids.c b/tests/main/snap-confine-privs/uids-and-gids.c new file mode 100644 index 00000000..053bb71e --- /dev/null +++ b/tests/main/snap-confine-privs/uids-and-gids.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +int main(int argc __attribute__((unused)), char* argv[] __attribute__((unused))) +{ + uid_t ruid, euid, suid; + gid_t rgid, egid, sgid; + if (getresuid(&ruid, &euid, &suid) < 0) { + perror("cannot call getresuid"); + exit(1); + } + if (getresgid(&rgid, &egid, &sgid) < 0) { + perror("cannot call getresgid"); + exit(1); + } + printf("ruid=%-5d euid=%-5d suid=%-5d rgid=%-5d egid=%-5d sgid=%-5d\n", ruid, euid, suid, rgid, egid, sgid); + return 0; +} diff --git a/tests/main/snap-confine/task.yaml b/tests/main/snap-confine/task.yaml new file mode 100644 index 00000000..eef0f77c --- /dev/null +++ b/tests/main/snap-confine/task.yaml @@ -0,0 +1,41 @@ +summary: Test that snap-confine errors in the right way + +# the error message can only happen on classic systems +systems: [-ubuntu-core-16-*] + +prepare: | + echo "Install test snap" + snap install test-snapd-tools + +restore: | + . $TESTSLIB/dirs.sh + + echo "Restore current symlink" + mv $SNAP_MOUNT_DIR/core/current.renamed $SNAP_MOUNT_DIR/core/current || true + rm -f snap-confine.stderr + +execute: | + . $TESTSLIB/dirs.sh + + echo "Simulating broken current symlink for core" + mv $SNAP_MOUNT_DIR/core/current $SNAP_MOUNT_DIR/core/current.renamed + + if test-snapd-tools.echo hello 2>snap-confine.stderr; then + echo "test-snapd-tools.echo should fail to run, test broken" + fi + cat snap-confine.stderr | MATCH 'cannot locate the core or legacy core snap \(current symlink missing\?\):' + + echo "Test nvidia device fix" + mv $SNAP_MOUNT_DIR/core/current.renamed $SNAP_MOUNT_DIR/core/current + # For https://github.com/snapcore/snapd/pull/4042 + echo "Simulate nvidia device tags" + mkdir -p /run/udev/tags/snap_test-snapd-tools_echo + for f in c226:0 +module:nvidia +module:nvidia_modeset; do + touch /run/udev/tags/snap_test-snapd-tools_echo/$f + done + test-snapd-tools.echo hello | MATCH hello + echo "Non nvidia files are still there" + test -f /run/udev/tags/snap_test-snapd-tools_echo/c226:0 + echo "But nvidia files are gone" + ! test -f /run/udev/tags/snap_test-snapd-tools_echo/+module:nvidia + ! test -f /run/udev/tags/snap_test-snapd-tools_echo/+module:nvidia_modeset diff --git a/tests/main/snap-connect/task.yaml b/tests/main/snap-connect/task.yaml new file mode 100644 index 00000000..6aadc0d7 --- /dev/null +++ b/tests/main/snap-connect/task.yaml @@ -0,0 +1,56 @@ +summary: Check that snap connect works + +prepare: | + . $TESTSLIB/snaps.sh + + echo "Install a test snap" + install_local home-consumer + # the home interface is not autoconnected on all-snap systems + if [[ ! "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + snap disconnect home-consumer:home + fi + +execute: | + CONNECTED_PATTERN=':home +home-consumer' + + echo "The plug can be connected to a matching slot of OS snap without snap:slot argument" + snap connect home-consumer:home + snap interfaces | MATCH "$CONNECTED_PATTERN" + + snap disconnect home-consumer:home + + echo "The plug can be connected to a matching slot with slot name omitted" + snap connect home-consumer:home + snap interfaces | MATCH "$CONNECTED_PATTERN" + + snap disconnect home-consumer:home + snap tasks --last=disconnect| MATCH "Disconnect .* from core:home" + + echo "The plug can be connected to a slot on the core snap using abbreviated syntax" + snap connect home-consumer:home :home + + snap interfaces | MATCH "$CONNECTED_PATTERN" + + snap tasks --last=connect| MATCH "Connect home-consumer:home to core:home" + + # NOTE: Those only work when installed from the store as otherwise we don't + # have snap declaration assertion and cannot check if a given connection + # should be allowed. + CONTENT_CONNECTED_PATTERN='test-snapd-content-slot:shared-content-slot +test-snapd-content-plug:shared-content-plug' + + echo "The plug side auto-connects when content is installed" + snap install --edge test-snapd-content-slot + snap install --edge test-snapd-content-plug + + snap tasks --last=install| MATCH "Mount snap \"test-snapd-content-plug\"" + + snap interfaces | MATCH "$CONTENT_CONNECTED_PATTERN" + + # Remove the content snaps so that we can reinstall them the other way around + snap remove test-snapd-content-plug + snap remove test-snapd-content-slot + + echo "The slot side auto-connects when content snap is installed" + snap install --edge test-snapd-content-plug + snap install --edge test-snapd-content-slot + snap interfaces | MATCH "$CONTENT_CONNECTED_PATTERN" diff --git a/tests/main/snap-debug-get-base-declaration/task.yaml b/tests/main/snap-debug-get-base-declaration/task.yaml new file mode 100644 index 00000000..4d9edac0 --- /dev/null +++ b/tests/main/snap-debug-get-base-declaration/task.yaml @@ -0,0 +1,9 @@ +summary: check that snap debug get-base-declaration works +details: | + The get-base-declaration debug action is intended for the security team so + that they can easily review the whole base declaration, however it is + stored inside the snapd code. +execute: | + snap debug get-base-declaration | MATCH 'type: base-declaration' + # The string "$builtin" terminates the declaration + snap debug get-base-declaration | MATCH '^\$builtin$' diff --git a/tests/main/snap-discard-ns/task.yaml b/tests/main/snap-discard-ns/task.yaml new file mode 100644 index 00000000..4594d0ab --- /dev/null +++ b/tests/main/snap-discard-ns/task.yaml @@ -0,0 +1,36 @@ +summary: check basic operation of snap-discard-ns +details: | + The internal command snap-discard-ns discards (unmounts) the + /run/snapd/ns/$SNAP_NAME.mnt file and removes the current mount profile + /run/snapd/ns/snap.$SNAP_NAME.fstab. The profile removal is optional and it + is not an error if it doesn't exist. +prepare: | + . $TESTSLIB/snaps.sh + install_local test-snapd-tools +execute: | + . "$TESTSLIB/dirs.sh" + + echo "We can try to discard a namespace before snap runs" + $LIBEXECDIR/snapd/snap-discard-ns test-snapd-tools + + echo "We can try to discard a namespace before the .mnt file exits" + mkdir -p /run/snapd/ns/ + $LIBEXECDIR/snapd/snap-discard-ns test-snapd-tools + + echo "We can try to discard a namespace before the .mnt file is mounted" + touch /run/snapd/ns/test-snapd-tools.mnt + $LIBEXECDIR/snapd/snap-discard-ns test-snapd-tools + + echo "We can discard the namespace after a snap runs" + test-snapd-tools.success + # The last hex is the same as nsfs but older stat on ubuntu 14.04 doesn't know + # proc is there because on older kernels /proc/*/ns/mnt is not on nsfs but still on procfs. + stat -f -c %T /run/snapd/ns/test-snapd-tools.mnt | MATCH 'proc|nsfs|0x6e736673' + $LIBEXECDIR/snapd/snap-discard-ns test-snapd-tools + stat -f -c %T /run/snapd/ns/test-snapd-tools.mnt | MATCH 'tmpfs' + + echo "We can fake a current mount profile and see that it is removed too" + test-snapd-tools.success + touch /run/snapd/ns/snap.test-snapd-tools.fstab + $LIBEXECDIR/snapd/snap-discard-ns test-snapd-tools + test ! -e /run/snapd/ns/snap.test-snapd-tools.fstab diff --git a/tests/main/snap-disconnect/task.yaml b/tests/main/snap-disconnect/task.yaml new file mode 100644 index 00000000..cb57803d --- /dev/null +++ b/tests/main/snap-disconnect/task.yaml @@ -0,0 +1,42 @@ +summary: Check that snap disconnect works + +systems: [-ubuntu-core-16-64] + +environment: + SNAP_FILE: "home-consumer_1.0_all.snap" + +prepare: | + echo "Install a test snap" + snap pack $TESTSLIB/snaps/home-consumer + snap install --dangerous $SNAP_FILE + +restore: | + rm -f *.snap + +execute: | + DISCONNECTED_PATTERN="\-\s+home-consumer:home" + + echo "Disconnect everything from given slot" + snap connect home-consumer:home core:home + snap disconnect core:home + snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN" + + echo "Disconnect everything from given slot (abbreviated)" + snap connect home-consumer:home core:home + snap disconnect :home + snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN" + + echo "Disconnect everything from given plug" + snap connect home-consumer:home core:home + snap disconnect home-consumer:home + snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN" + + echo "Disconnect specific plug and slot" + snap connect home-consumer:home core:home + snap disconnect home-consumer:home core:home + snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN" + + echo "Disconnect specific plug and slot (abbreviated)" + snap connect home-consumer:home core:home + snap disconnect home-consumer:home :home + snap interfaces | grep -Pzqe "$DISCONNECTED_PATTERN" diff --git a/tests/main/snap-download/task.yaml b/tests/main/snap-download/task.yaml new file mode 100644 index 00000000..57540e91 --- /dev/null +++ b/tests/main/snap-download/task.yaml @@ -0,0 +1,35 @@ +summary: Check that snap download works +execute: | + verify_asserts() { + fn="$1" + MATCH "type: account-key" < "$fn" + MATCH "type: snap-declaration" < "$fn" + MATCH "type: snap-revision" < "$fn" + } + echo "Snap download can download snaps" + snap download test-snapd-control-consumer + ls test-snapd-control-consumer_*.snap + verify_asserts test-snapd-control-consumer_*.assert + + echo "Snap will use existing files" + SNAPD_DEBUG=1 snap download test-snapd-control-consumer 2>&1 | MATCH "not downloading, using existing file" + + echo "Snap download understand --edge" + snap download --edge test-snapd-tools + ls test-snapd-tools_*.snap + verify_asserts test-snapd-tools_*.assert + + echo "Snap download downloads devmode snaps" + snap download --beta classic + ls classic_*.snap + verify_asserts classic_*.assert + + echo "Snap download can download snaps as user" + su -l -c "SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE HTTPS_PROXY=$HTTPS_PROXY snap download test-snapd-tools" test + ls /home/test/test-snapd-tools_*.snap + verify_asserts /home/test/test-snapd-tools_*.assert +restore: | + rm -f *.snap + rm -f *.assert + rm -f ~test/*.snap + rm -f ~test/*.assert diff --git a/tests/main/snap-env/task.yaml b/tests/main/snap-env/task.yaml new file mode 100644 index 00000000..43795a58 --- /dev/null +++ b/tests/main/snap-env/task.yaml @@ -0,0 +1,54 @@ +summary: inspect all the set environment variables prefixed with SNAP_ and XDG_ +prepare: | + snap pack $TESTSLIB/snaps/test-snapd-tools + snap install --dangerous test-snapd-tools_1.0_all.snap +restore: | + rm -f *.snap + rm -f *-vars.txt +debug: | + cat *-vars.txt +execute: | + echo "Collect SNAP and XDG environment variables" + test-snapd-tools.env | egrep '^SNAP_' | sort > snap-vars.txt + test-snapd-tools.env | egrep '^XDG_' | sort > xdg-vars.txt + test-snapd-tools.env | egrep '^EXTRA_' | sort > extra-vars.txt + + echo "Collect PATH and HOME environment variables" + test-snapd-tools.env | egrep '^(SNAP|PATH|HOME)=' | sort > misc-vars.txt + + echo "Ensure that SNAP environment variables are what we expect" + MATCH '^SNAP_ARCH=(amd64|i386|arm64|armhf|ppc64el)$' < snap-vars.txt + MATCH '^SNAP_COMMON=/var/snap/test-snapd-tools/common$' < snap-vars.txt + MATCH '^SNAP_DATA=/var/snap/test-snapd-tools/x1$' < snap-vars.txt + MATCH '^SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void$' < snap-vars.txt + MATCH '^SNAP_NAME=test-snapd-tools$' < snap-vars.txt + # XXX: probably not something we ought to test + # egrep -q '^SNAP_REEXEC=0$' snap-vars.txt + MATCH '^SNAP_REVISION=x1$' < snap-vars.txt + MATCH '^SNAP_USER_COMMON=/root/snap/test-snapd-tools/common$' < snap-vars.txt + MATCH '^SNAP_USER_DATA=/root/snap/test-snapd-tools/x1$' < snap-vars.txt + MATCH '^SNAP_VERSION=1.0$' < snap-vars.txt + CTX=$(cat /var/lib/snapd/cookie/snap.test-snapd-tools) + MATCH "^SNAP_COOKIE=$CTX" < snap-vars.txt + MATCH "^SNAP_CONTEXT=$CTX" < snap-vars.txt + test $(wc -l < snap-vars.txt) -eq 12 + + echo "Enure that XDG environment variables are what we expect" + MATCH '^XDG_RUNTIME_DIR=/run/user/0/snap.test-snapd-tools$' < xdg-vars.txt + test $(wc -l < xdg-vars.txt) -ge 1 + + echo "Enure that EXTRA environment variables are what we expect" + MATCH '^EXTRA_GLOBAL=extra-global' < extra-vars.txt + MATCH '^EXTRA_LOCAL=extra-local' < extra-vars.txt + MATCH '^EXTRA_LOCAL_NESTED=extra-global-nested' < extra-vars.txt + MATCH "^EXTRA_CACHE_DIR=$HOME/snap/test-snapd-tools/x1/.cache" < extra-vars.txt + test $(wc -l < extra-vars.txt) -eq 4 + + echo "Ensure that TMPDIR is not passed through to a confined snap" + TMPDIR=/foobar test-snapd-tools.env | grep -qv ^TMPDIR= + + echo "Ensure that SNAP, PATH and HOME are what we expect" + MATCH "^SNAP=/snap/test-snapd-tools/x1$" < misc-vars.txt + MATCH '^PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games$' < misc-vars.txt + MATCH '^HOME=/root/snap/test-snapd-tools/x1$' < misc-vars.txt + test $(wc -l < misc-vars.txt) -eq 3 diff --git a/tests/main/snap-get/task.yaml b/tests/main/snap-get/task.yaml new file mode 100644 index 00000000..7a765154 --- /dev/null +++ b/tests/main/snap-get/task.yaml @@ -0,0 +1,106 @@ +summary: Check that `snap get` works as expected + +prepare: | + snap install --devmode jq + + echo "Build basic test package (without hooks)" + snap pack $TESTSLIB/snaps/basic + snap install --dangerous basic_1.0_all.snap + + echo "Build package with hook to run snapctl set" + snap pack $TESTSLIB/snaps/snapctl-hooks + snap install --dangerous snapctl-hooks_1.0_all.snap + +restore: | + rm -f basic_1.0_all.snap + rm -f snapctl-hooks_1.0_all.snap + +execute: | + echo "Test that snap get fails on a snap without any hooks" + if output=$(snap get basic foo); then + echo "snap get unexpectedly worked with output '$output'" + exit 1 + fi + + echo "Test that getting root document without any configuration produces an error with list format" + if output=$(snap get snapctl-hooks 2>&1); then + echo "snap get didn't fail as expected" + exit 1 + fi + expected="error: snap \"snapctl-hooks\" has no configuration" + if [ "$output" != "$expected" ]; then + echo "Expected '$expected' error, but it was '$output'" + exit 1 + fi + + echo "Test that getting root document without any configuration works for json output" + snap get snapctl-hooks -d | MATCH "^{}$" + + echo "Test that values set via snapctl can be obtained via snap get" + if ! snap set snapctl-hooks command=test-snapctl-set-foo; then + echo "snap set unexpectedly failed" + exit 1 + fi + if ! output=$(snap get snapctl-hooks command); then + echo "snap get unexpectedly failed" + exit 1 + fi + expected="test-snapctl-set-foo" + if [ "$output" != "$expected" ]; then + echo "Expected 'command' to be '$expected', but it was '$output'" + exit 1 + fi + if ! output=$(snap get snapctl-hooks foo); then + echo "snap get unexpectedly failed" + exit 1 + fi + expected="bar" + if [ "$output" != "$expected" ]; then + echo "Expected 'foo' to be '$expected', but it was '$output'" + exit 1 + fi + + echo "Test that keys of json documents can be obtained via snap get" + if ! snap set snapctl-hooks command=test-snapctl-set-bar-doc; then + echo "snap set unexpectedly failed" + exit 1 + fi + snap get snapctl-hooks bar 2>&1 | MATCH -z "WARNING" + snap get snapctl-hooks -l bar 2>&1 | MATCH -z "^Key.*Value.*bar.a.*{\.\.\.}.*bar.b.*3" + snap get snapctl-hooks -d bar | MATCH -z "^{.*\"bar\": {.*\"a\": {.*\"aa\": 1,.*\"ab\": 2.*},.*\"b\": 3.*}.*}" + + snap get snapctl-hooks bar.a.aa | MATCH "^1$" + snap get snapctl-hooks bar.a.ab | MATCH "^2$" + + echo "Test that root document can be obtained via snap get" + snap get snapctl-hooks -l 2>&1 | MATCH -z "^Key.*Value.*bar.*{\.\.\.}.*command.*test-snapctl-set-bar-doc.*foo.*bar" + snap get snapctl-hooks -d | MATCH -z "^{.*\"bar\": {.*\"a\": {.*\"aa\": 1,.*\"ab\": 2.*},.*\"b\": 3.*}.*,.*\"command\": \"test-snapctl-set-bar-doc\",.*\"foo\": \"bar\".*}" + + echo "Test number formats" + if ! snap set snapctl-hooks command=test-get-int intnumber=1234567890 intnumber2="{\"a\":9876543210}"; then + echo "snap set unexpectedly failed" + exit 1 + fi + if ! output=$(snap get snapctl-hooks intnumber); then + echo "snap get unexpectedly failed" + fi + expected="1234567890" + if [ "$output" != "$expected" ]; then + echo "Expected 'intnumber' to be '$expected', but it was '$output'" + exit 1 + fi + + if ! output=$(snap get snapctl-hooks intnumber2); then + echo "snap get unexpectedly failed" + fi + echo "$output" | MATCH ".*\"a\": 9876543210.*" + + echo "Ensure config value has correct format" + jq ".data[\"config\"][\"snapctl-hooks\"].intnumber" /var/lib/snapd/state.json | MATCH "1234567890" + + echo "Test that config values are not available once snap is removed" + snap remove snapctl-hooks + if output=$(snap get snapctl-hooks foo); then + echo "Expected snap get to fail, but got '$output'" + exit 1 + fi diff --git a/tests/main/snap-info/check.py b/tests/main/snap-info/check.py new file mode 100644 index 00000000..fdaa021f --- /dev/null +++ b/tests/main/snap-info/check.py @@ -0,0 +1,141 @@ +import os +import re +import sys +import yaml + +def die(s): + print(s, file=sys.stderr) + sys.exit(1) + +def equals(name, s1, s2): + if s1 != s2: + die("in %s expected %r, got %r" % (name, s2, s1)) + +def matches(name, s, r): + if not re.search(r, s): + die("in %s expected to match %s, got %r" % (name, r, s)) + +def check(name, d, *a): + ka = set() + for k, op, *args in a: + if op == maybe: + d[k] = d.get(k,"") + if k not in d: + die("in %s expected to have a key %r" % (name, k)) + op(name+"."+k, d[k], *args) + ka.add(k) + kd = set(d) + if ka < kd: + die("in %s: extra keys: %r" % (name, kd-ka)) + +def exists(name, d): + pass + +def maybe(name, d): + pass + + +verNotesRx = re.compile(r"^\w\S*\s+-$") +def verRevNotesRx(s): + return re.compile(r"^\w\S*\s+\(\d+\)\s+[1-9][0-9]*\w+\s+" + s + "$") + +if os.environ['SNAPPY_USE_STAGING_STORE'] == '1': + snap_ids={ + "test-snapd-tools": "02AHdOomTzby7gTaiLX3M3SGMmXDfLJp", + "test-snapd-devmode": "FcHyKyMiQh71liP8P82SsyMXtZI5mvVj", + "test-snapd-python-webserver": "uHjTANBWSXSiYzNOUXZNDnOSH3POSqWS", + } +else: + snap_ids={ + "test-snapd-tools": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "test-snapd-devmode": "821MII7GAzoRnPvTEb8R51Z1s9e0XmK5", + "test-snapd-python-webserver": "Wcs8QL2iRQMjsPYQ4qz4V1uOlElZ1ZOb", + } + +res = list(yaml.load_all(sys.stdin)) + +equals("number of entries", len(res), 7) + +check("basic", res[0], + ("name", equals, "basic"), + ("summary", equals, "Basic snap"), + ("path", matches, r"^basic_[0-9.]+_all\.snap$"), + ("version", matches, verNotesRx), +) + +check("basic-desktop", res[1], + ("name", equals, "basic-desktop"), + ("path", matches, "snaps/basic-desktop/$"), # note the trailing slash + ("summary", equals, ""), + ("version", matches, verNotesRx), +) + +check("test-snapd-tools", res[2], + ("name", equals, "test-snapd-tools"), + ("publisher", equals, "canonical"), + ("contact", equals, "snappy-canonical-storeaccount@canonical.com"), + ("summary", equals, "Tools for testing the snapd application"), + ("description", equals, "A tool to test snapd\n"), + ("commands", exists), + ("tracking", equals, "stable"), + ("installed", matches, verRevNotesRx("-")), + ("refreshed", exists), + ("channels", check, + ("stable", matches, verRevNotesRx("-")), + ("candidate", equals, "↑"), + ("beta", equals, "↑"), + ("edge", matches, verRevNotesRx("-")), + ), + ("snap-id", equals, snap_ids["test-snapd-tools"]), +) + +check("test-snapd-devmode", res[3], + ("name", equals, "test-snapd-devmode"), + ("publisher", equals, "canonical"), + ("contact", equals, "snappy-canonical-storeaccount@canonical.com"), + ("summary", equals, "Basic snap with devmode confinement"), + ("description", equals, "A basic buildable snap that asks for devmode confinement\n"), + ("tracking", equals, "beta"), + ("installed", matches, verRevNotesRx("devmode")), + ("refreshed", exists), + ("channels", check, + ("stable", equals, "–"), + ("candidate", equals, "–"), + ("beta", matches, verRevNotesRx("devmode")), + ("edge", matches, verRevNotesRx("devmode")), + ), + ("snap-id", equals, snap_ids["test-snapd-devmode"]), +) + +check("core", res[4], + ("name", equals, "core"), + ("type", equals, "core"), # attenti al cane + ("publisher", exists), + ("summary", exists), + ("description", exists), + ("tracking", exists), + ("installed", exists), + ("refreshed", exists), + ("channels", exists), + # contacts is set on classic but not on Ubuntu Core where we + # sideload "core" + ("contact", maybe), + ("snap-id", maybe), +) + +check("error", res[5], + ("argument", equals, "/etc/passwd"), + ("warning", equals, "not a valid snap"), +) + +# not installed snaps have "contact" information +check("test-snapd-python-webserver", res[6], + ("name", equals, "test-snapd-python-webserver"), + ("publisher", equals, "canonical"), + ("contact", equals, "snappy-canonical-storeaccount@canonical.com"), + ("summary", exists), + ("description", exists), + ("channels", exists), + ("snap-id", equals, snap_ids["test-snapd-python-webserver"]), + +) diff --git a/tests/main/snap-info/task.yaml b/tests/main/snap-info/task.yaml new file mode 100644 index 00000000..acf3a792 --- /dev/null +++ b/tests/main/snap-info/task.yaml @@ -0,0 +1,29 @@ +summary: Check that snap info works + +prepare: | + . $TESTSLIB/pkgdb.sh + distro_install_package python3-yaml + + snap pack $TESTSLIB/snaps/basic + snap install test-snapd-tools + snap install --channel beta --devmode test-snapd-devmode + +restore: | + rm -f basic_1.0_all.snap + . $TESTSLIB/pkgdb.sh + distro_purge_package python3-yaml + rm -f out + +execute: | + echo "With no arguments, errors out" + snap info && exit 1 || true + + echo "With one non-snap argument, errors out" + snap info /etc/passwd && exit 1 || true + + snap info basic_1.0_all.snap $TESTSLIB/snaps/basic-desktop test-snapd-tools test-snapd-devmode core /etc/passwd test-snapd-python-webserver > out + python3 check.py < out + + snap info --verbose $TESTSLIB/snaps/basic-desktop + snap info --verbose basic_1.0_all.snap|MATCH "sha3-384:" + snap info --verbose test-snapd-tools|MATCH " ignore-validation:" diff --git a/tests/main/snap-interface/snap-interface-core-support.yaml b/tests/main/snap-interface/snap-interface-core-support.yaml new file mode 100644 index 00000000..36073292 --- /dev/null +++ b/tests/main/snap-interface/snap-interface-core-support.yaml @@ -0,0 +1,6 @@ +name: core-support +summary: special permissions for the core snap +plugs: + - core:core-support-plug +slots: + - core diff --git a/tests/main/snap-interface/task.yaml b/tests/main/snap-interface/task.yaml new file mode 100644 index 00000000..86ecb968 --- /dev/null +++ b/tests/main/snap-interface/task.yaml @@ -0,0 +1,11 @@ +summary: Check that "snap interface" works as expected +details: | + The "snap interface" command displays a listing of used interfaces +execute: | + snap interface | MATCH 'core-support\s+ special permissions for the core snap' + snap interface --all | MATCH 'classic-support\s+ special permissions for the classic snap' + snap interface core-support > out.yaml + diff -u out.yaml snap-interface-core-support.yaml +restore: | + rm -f out.yaml + diff --git a/tests/main/snap-multi-service-failing/task.yaml b/tests/main/snap-multi-service-failing/task.yaml new file mode 100644 index 00000000..c631bf46 --- /dev/null +++ b/tests/main/snap-multi-service-failing/task.yaml @@ -0,0 +1,10 @@ +summary: | + Check that `snap install` doesn't leave a service running when the install fails. + +execute: | + . $TESTSLIB/snaps.sh + echo "when a snap install fails" + ! install_local test-snapd-multi-service + + echo "we don't leave a service running" + ! systemctl is-active snap.test-snapd-multi-service.ok.service diff --git a/tests/main/snap-on-non-shared-root/task.yaml b/tests/main/snap-on-non-shared-root/task.yaml new file mode 100644 index 00000000..497d92ca --- /dev/null +++ b/tests/main/snap-on-non-shared-root/task.yaml @@ -0,0 +1,34 @@ +summary: Ensure that snapd works on systems with a non rshared root +# no need to run on ubuntu-core-16, we always have / shared here +systems: [-ubuntu-core-*] +prepare: | + . $TESTSLIB/dirs.sh + # simulate a system with a non-shared / + mount --make-private / + mount --make-private $(readlink -f $SNAP_MOUNT_DIR/core/current) +restore: | + . $TESTSLIB/dirs.sh + mount --make-rshared / + mount --make-rshared $(readlink -f $SNAP_MOUNT_DIR/core/current) +execute: | + . $TESTSLIB/dirs.sh + + echo "Install fresh test-snapd-tools" + snap install test-snapd-tools + test-snapd-tools.echo hello + + echo "Refresh, subsequent runs after refresh will fail if / is not rshared" + snap refresh --edge test-snapd-tools + test-snapd-tools.echo hello + + echo "Ensure we have a shared mount of $SNAP_MOUNT_DIR" + cat /proc/self/mountinfo |MATCH "$SNAP_MOUNT_DIR $SNAP_MOUNT_DIR.*shared:[0-9]" + + echo "Run it again for good measure" + test-snapd-tools.echo hello + echo "... and ensure we do not mount $SNAP_MOUNT_DIR again" + n=$(cat /proc/self/mountinfo |grep "$SNAP_MOUNT_DIR $SNAP_MOUNT_DIR.*shared:[0-9]"|wc -l) + if [ "$n" -ne 1 ]; then + echo "Incorrect extra $SNAP_MOUNT_DIR bind mounts created" + exit 1 + fi \ No newline at end of file diff --git a/tests/main/snap-readme/task.yaml b/tests/main/snap-readme/task.yaml new file mode 100644 index 00000000..19f28fda --- /dev/null +++ b/tests/main/snap-readme/task.yaml @@ -0,0 +1,10 @@ +summary: the /snap directory has a magic README file +details: > + We found that some users are genuinely confused and concerned about the + /snap directory or the disk space it appears to be using. Snapd now + maintains a README file with some useful hints about what is going on that + will, hopefully, help people understand this better. +execute: | + snap version # To ensure that snapd is awake + . $TESTSLIB/dirs.sh + MATCH "https://forum.snapcraft.io/t/the-snap-directory/2817" "$SNAP_MOUNT_DIR/README" diff --git a/tests/main/snap-remove-not-mounted/task.yaml b/tests/main/snap-remove-not-mounted/task.yaml new file mode 100644 index 00000000..4f6207d4 --- /dev/null +++ b/tests/main/snap-remove-not-mounted/task.yaml @@ -0,0 +1,15 @@ +summary: Ensure remove with unmounted base dir works + +execute: | + cp -ar $TESTSLIB/snaps/test-snapd-tools /tmp + snap try /tmp/test-snapd-tools + + . $TESTSLIB/dirs.sh + + # simulate what happens if someone "snap try /tmp/something" and then + # reboots: the dir is gone and nothing can be mounted anymore + rm -rf /tmp/test-snapd-tools + umount $SNAP_MOUNT_DIR/test-snapd-tools/x1 + + # ensure removal still works + snap remove test-snapd-tools \ No newline at end of file diff --git a/tests/main/snap-repair/task.yaml b/tests/main/snap-repair/task.yaml new file mode 100644 index 00000000..5822a987 --- /dev/null +++ b/tests/main/snap-repair/task.yaml @@ -0,0 +1,22 @@ +summary: Ensure that snap-repair is available + +systems: [-fedora-*, -opensuse-*] + +execute: | + . $TESTSLIB/dirs.sh + + if ! grep -q "ID=ubuntu-core" /etc/os-release; then + echo "Ensure snap-repair is disabled on classic" + "${LIBEXECDIR}"/snapd/snap-repair 2>&1 | MATCH "cannot use snap-repair on a classic system" + exit 0 + fi + + # All the tests below are only relevant on an ubuntu-core system + + echo "Check that the snap-repair timer is active" + systemctl list-timers | MATCH snapd.snap-repair.timer + + echo "Check that snap-repair can be run" + "${LIBEXECDIR}"/snapd/snap-repair run + + diff --git a/tests/main/snap-run-alias/task.yaml b/tests/main/snap-run-alias/task.yaml new file mode 100644 index 00000000..a10e9ccb --- /dev/null +++ b/tests/main/snap-run-alias/task.yaml @@ -0,0 +1,39 @@ +summary: Check that alias symlinks work correctly + +systems: [-ubuntu-core-16-*] + +prepare: | + echo Ensure we have a os snap with snap run + $TESTSLIB/reset.sh + snap install --channel=beta core + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + +restore: | + . $TESTSLIB/dirs.sh + rm -f $SNAP_MOUNT_DIR/bin/test_echo + rm -f $SNAP_MOUNT_DIR/bin/test_cat + rm -f orig.txt + rm -f new.txt + +environment: + APP/testsnapdtoolsecho: test-snapd-tools.echo + APP/testsnapdtoolscat: test-snapd-tools.cat + ALIAS/testsnapdtoolsecho: test_echo + ALIAS/testsnapdtoolscat: test_cat + +execute: | + . $TESTSLIB/dirs.sh + + SNAP=$SNAP_MOUNT_DIR/test-snapd-tools/current + + echo Testing that creating an alias symlinks works + $APP $SNAP/bin/cat + $APP $SNAP/bin/cat > orig.txt 2>&1 + + ln -s $APP $SNAP_MOUNT_DIR/bin/$ALIAS + + $ALIAS $SNAP/bin/cat + $ALIAS $SNAP/bin/cat > new.txt 2>&1 + + diff -u orig.txt new.txt diff --git a/tests/main/snap-run-hook/task.yaml b/tests/main/snap-run-hook/task.yaml new file mode 100644 index 00000000..22b7cd09 --- /dev/null +++ b/tests/main/snap-run-hook/task.yaml @@ -0,0 +1,52 @@ +summary: Check that `snap run` can actually run hooks + +environment: + # Ensure that running purely from the deb (without re-exec) works + # correctly + SNAP_REEXEC/reexec0: 0 + SNAP_REEXEC/reexec1: 1 + ENVDUMP: /var/snap/basic-hooks/current/hooks-env + +prepare: | + echo "Build test hooks package" + snap pack $TESTSLIB/snaps/basic-hooks + snap install --dangerous basic-hooks_1.0_all.snap + +restore: | + rm -f basic-hooks_1.0_all.snap + +execute: | + # Note that `snap run` doesn't exit non-zero if the hook is missing, so we + # check the output instead. + + echo "Test that snap run can call valid hooks" + + if ! output="$(snap run --hook=configure basic-hooks)"; then + echo "Failed to run configure hook" + exit 1 + fi + + expected_output="configure hook" + if [ "$output" != "$expected_output" ]; then + echo "Expected configure output to be '$expected_output', but it was '$output'" + exit 1 + fi + + echo "Test that snap run cannot call invalid hooks" + + if output="$(snap run --hook=invalid-hook basic-hooks)"; then + echo "Expected snap run to fail upon missing hook, but it was '$output'" + exit 1 + fi + + expected_output="" + if [ "$output" != "$expected_output" ]; then + echo "Expected invalid-hook output to be '$expected_output', but it was '$output'" + exit 1 + fi + + snap set basic-hooks command=dump-env + echo "Test that environment variables were interpolated" + cat $ENVDUMP | MATCH "^TEST_COMMON=/var/snap/basic-hooks/common$" + cat $ENVDUMP | MATCH "^TEST_DATA=/var/snap/basic-hooks/.*$" + cat $ENVDUMP | MATCH "^TEST_SNAP=/snap/basic-hooks/.*$" diff --git a/tests/main/snap-run-symlink-error/task.yaml b/tests/main/snap-run-symlink-error/task.yaml new file mode 100644 index 00000000..5ba46a61 --- /dev/null +++ b/tests/main/snap-run-symlink-error/task.yaml @@ -0,0 +1,21 @@ +summary: Check error handling in symlinks to /usr/bin/snap +restore: | + . $TESTSLIB/dirs.sh + rm -f $SNAP_MOUNT_DIR/bin/xxx + rmdir $SNAP_MOUNT_DIR/bin +execute: | + . $TESTSLIB/dirs.sh + echo Setting up incorrect symlink for snap run + mkdir -p $SNAP_MOUNT_DIR/bin + ln -s /usr/bin/snap $SNAP_MOUNT_DIR/bin/xxx + echo Running unknown command + expected="internal error, please report: running \"xxx\" failed: cannot find current revision for snap xxx: readlink $SNAP_MOUNT_DIR/xxx/current: no such file or directory" + output="$($SNAP_MOUNT_DIR/bin/xxx 2>&1 )" && exit 1 + echo $output + err=$? + echo Verifying error message + if [ $err -ne 46 ]; then + echo Wrong error code $err + fi + [ "$output" = "$expected" ] || exit 1 + diff --git a/tests/main/snap-run-symlink/task.yaml b/tests/main/snap-run-symlink/task.yaml new file mode 100644 index 00000000..ba88e504 --- /dev/null +++ b/tests/main/snap-run-symlink/task.yaml @@ -0,0 +1,34 @@ +summary: Check that symlinks to /usr/bin/snap trigger `snap run` + +systems: [-ubuntu-core-16-*] + +prepare: | + echo Ensure we have a os snap with snap run + $TESTSLIB/reset.sh + snap install --channel=beta core + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + +environment: + APP/testsnapdtoolsecho: test-snapd-tools.echo + APP/testsnapdtoolscat: test-snapd-tools.cat + +execute: | + . $TESTSLIB/dirs.sh + SNAP=$SNAP_MOUNT_DIR/test-snapd-tools/current + + echo Testing that replacing the wrapper with a symlink works + $APP $SNAP/bin/cat + $APP $SNAP/bin/cat > orig.txt 2>&1 + + rm $SNAP_MOUNT_DIR/bin/$APP + ln -s /usr/bin/snap $SNAP_MOUNT_DIR/bin/$APP + + $APP $SNAP/bin/cat + $APP $SNAP/bin/cat > new.txt 2>&1 + + diff -u orig.txt new.txt + +restore: | + rm -f orig.txt + rm -f new.txt diff --git a/tests/main/snap-run-userdata-current/task.yaml b/tests/main/snap-run-userdata-current/task.yaml new file mode 100644 index 00000000..0e0dbfb1 --- /dev/null +++ b/tests/main/snap-run-userdata-current/task.yaml @@ -0,0 +1,40 @@ +summary: Check that 'current' symlink is created with 'snap run' + +systems: [-ubuntu-core-16-*] + +prepare: | + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + +execute: | + . $TESTSLIB/dirs.sh + echo "Test that 'current' symlink is created in user data dir" + CURRENT=$(readlink $SNAP_MOUNT_DIR/test-snapd-tools/current) + if [ -z "$CURRENT" ]; then + echo "Could not determine current version of $SNAP" + exit 1 + fi + + $SNAP_MOUNT_DIR/bin/test-snapd-tools.echo -n + UDATA_CURRENT=$(readlink $HOME/snap/test-snapd-tools/current) + if [ "$CURRENT" != "$UDATA_CURRENT" ]; then + echo "Invalid 'current' symlink in user-data directory, expected $CURRENT, got $UDATA_CURRENT" + exit 1 + fi + + echo "Test that 'current' symlink is recreated" + rm -rf $HOME/snap/test-snapd-tools/current + $SNAP_MOUNT_DIR/bin/test-snapd-tools.echo -n + if [ ! -L $HOME/snap/test-snapd-tools/current ]; then + echo "The 'current' symlink not present in user-data directory" + exit 1 + fi + + echo "Test that 'current' symlink is updated if incorrect" + ln -fs $HOME/snap/test-snapd-tools/wrong $HOME/snap/test-snapd-tools/current + $SNAP_MOUNT_DIR/bin/test-snapd-tools.echo -n + UDATA_CURRENT=$(readlink $HOME/snap/test-snapd-tools/current) + if [ "$CURRENT" != "$UDATA_CURRENT" ]; then + echo "Invalid 'current' symlink in user-data directory, expected $CURRENT, got $UDATA_CURRENT" + exit 1 + fi diff --git a/tests/main/snap-run/task.yaml b/tests/main/snap-run/task.yaml new file mode 100644 index 00000000..2735afea --- /dev/null +++ b/tests/main/snap-run/task.yaml @@ -0,0 +1,9 @@ +summary: Check that `snap run` runs + +prepare: | + . $TESTSLIB/snaps.sh + install_local basic-run + +execute: | + echo "Test that snap run use environments" + basic-run.echo-data | MATCH ^/var/snap diff --git a/tests/main/snap-seccomp/task.yaml b/tests/main/snap-seccomp/task.yaml new file mode 100644 index 00000000..afbe6681 --- /dev/null +++ b/tests/main/snap-seccomp/task.yaml @@ -0,0 +1,138 @@ +summary: Ensure that the snap-seccomp bpf handling works + +# FIXME: once $(snap debug confinment) can be used (in 2.27+) remove +# the systems line +systems: [ubuntu-*] + +environment: + PROFILE: /var/lib/snapd/seccomp/bpf/snap.test-snapd-tools.echo + SNAP_SECCOMP: /usr/lib/snapd/snap-seccomp + +execute: | + echo "Install test-snapd-tools and verify it works" + snap install test-snapd-tools + test-snapd-tools.echo hello | MATCH hello + + # FIXME: use dirs.sh in 2.27+ + echo "Ensure snap-seccomp is statically linked" + if ldd /usr/lib/snapd/snap-seccomp | MATCH libseccomp ; then + echo "found dynamically linked libseccomp, we need a staticly linked one" + exit 1 + fi + + # from the old test_complain + echo "Test that the @complain keyword works" + rm -f ${PROFILE}.bin + cat >"${PROFILE}.src" <"${PROFILE}.src" <"${PROFILE}.src" <"${PROFILE}.src" <&1 ); then + echo "test-snapd-tools.echo should fail with invalid seccomp profile" + exit 1 + fi + echo $output | MATCH "cannot apply seccomp profile: Invalid argument" + + echo "Add huge snapd.test-snapd-tools.bin to ensure size limit works" + dd if=/dev/zero of=${PROFILE}.bin count=50 bs=1M + if output=$(test-snapd-tools.echo hello 2>&1 ); then + echo "test-snapd-tools.echo should fail with big seccomp profile" + exit 1 + fi + echo $output | MATCH "profile .* exceeds .* bytes" + + + echo "Ensure the code cannot not run with a missing .bin profile" + rm -f ${PROFILE}.bin + if test-snapd-tools.echo hello; then + echo "filtering broken: program should have failed to run" + exit 1 + fi + + echo "Ensure the code cannot not run with an empty seccomp profile" + rm -f ${PROFILE}.bin + echo "" > ${PROFILE}.src + $SNAP_SECCOMP compile ${PROFILE}.src ${PROFILE}.bin + if test-snapd-tools.echo hello; then + echo "filtering broken: program should have failed to run" + exit 1 + fi + + echo "Ensure snap-confine waits for security profiles to appear" + rm -f ${PROFILE}.bin + cat >"${PROFILE}.src" <&1); then + echo "Expected usage of an invalid key to result in an error" + exit 1 + fi + [[ "$obtained" == *"invalid option name"* ]] + + echo "Install should fail altogether as it has a broken hook" + if obtained=$(snap install --dangerous failing-config-hooks_1.0_all.snap 2>&1); then + echo "Expected install of snap with broken configure hook to fail" + exit 1 + fi + [[ "$obtained" == *"error from within configure hook"* ]] diff --git a/tests/main/snap-sign/create-key.exp b/tests/main/snap-sign/create-key.exp new file mode 100644 index 00000000..4b1f8d60 --- /dev/null +++ b/tests/main/snap-sign/create-key.exp @@ -0,0 +1,17 @@ +spawn snap create-key + +expect "Passphrase: " +sleep .5 +send "pass\n" + +expect "Confirm passphrase: " +sleep .5 +send "pass\n" + +set status [wait] +if {[lindex $status 3] != 0} { + exit 1 +} + +set timeout 60 + diff --git a/tests/main/snap-sign/sign-model.exp b/tests/main/snap-sign/sign-model.exp new file mode 100644 index 00000000..f6fdc927 --- /dev/null +++ b/tests/main/snap-sign/sign-model.exp @@ -0,0 +1,20 @@ +spawn bash + +expect { + "# " { send "cat pi3-model.json | snap sign -k default &> pi3.model ; cat pi3.model\n" } +} + +# fun! +# gpg1 asks for a passphrase on the terminal no matter what +# gpg2 gets the passphrase via our fake pinentry +expect { + "Enter passphrase: " {send "pass\n"; exp_continue} + "type: model" { send "exit\n" } + timeout { exit 1 } + eof { exit 1 } +} + +set status [wait] +if {[lindex $status 3] != 0} { + exit 1 +} diff --git a/tests/main/snap-sign/task.yaml b/tests/main/snap-sign/task.yaml new file mode 100644 index 00000000..71c33023 --- /dev/null +++ b/tests/main/snap-sign/task.yaml @@ -0,0 +1,42 @@ +summary: Run snap sign to sign a model assertion + +# ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el, -fedora-*, -opensuse-*] + +prepare: | + . "$TESTSLIB"/mkpinentry.sh + +debug: | + sysctl kernel.random.entropy_avail || true + +execute: | + echo "Creating a new key without a password" + expect -f create-key.exp + + echo "Ensure we have the new key" + snap keys|MATCH default + key=$(snap keys|grep default|tr -s ' ' |cut -f2 -d' ') + + echo "Create an example model assertion" + cat <pi3-model.json + { + "type": "model", + "authority-id": "test", + "brand-id": "test", + "series": "16", + "model": "pi3", + "architecture": "armhf", + "gadget": "pi3", + "kernel": "pi2-kernel", + "timestamp": "$(date --utc '+%FT%T%:z')" + } + EOF + echo "Sign the model assertion with our key" + expect -d -f sign-model.exp + + echo "Verify that the resulting model assertion is signed" + MATCH "sign-key-sha3-384: $key" < pi3.model + +restore: | + rm -f pi3.model + rm -f pi3-model.json diff --git a/tests/main/snap-switch/task.yaml b/tests/main/snap-switch/task.yaml new file mode 100644 index 00000000..9de54d24 --- /dev/null +++ b/tests/main/snap-switch/task.yaml @@ -0,0 +1,8 @@ +summary: Ensure that the snap switch command works + +execute: | + snap install test-snapd-tools + snap info test-snapd-tools|MATCH "tracking: +stable" + + snap switch --edge test-snapd-tools + snap info test-snapd-tools|MATCH "tracking: +edge" diff --git a/tests/main/snap-update-ns/task.yaml b/tests/main/snap-update-ns/task.yaml new file mode 100644 index 00000000..0ae195e3 --- /dev/null +++ b/tests/main/snap-update-ns/task.yaml @@ -0,0 +1,77 @@ +summary: smoke test for snap-update-ns +details: | + Snapd is growing a new executable, snap-update-ns, to modify an existing + mount namespace. This is further documented on the forum here + https://forum.snapcraft.io/t/fixing-live-propagation-of-mount-changes/23 + + While the implementation matures this test checks that we call setns + correctly (and it doesn't fail) enough that we reach the "not implemented" + message that is currently in snap-updates-ns +environment: + # I made far too many typos when those were literals in the code below. + PLUG_SNAP: test-snapd-content-plug + SLOT_SNAP: test-snapd-content-slot +prepare: | + . $TESTSLIB/snaps.sh + # NOTE: those are installed locally so that they are not connected because + # of missing assertions. We are installing the slot before the plug snap so + # that there's no attempt to load the default provider. Just in case + # something changes we're disconnecting them so that tests are predictable. + install_local $SLOT_SNAP + install_local $PLUG_SNAP + snap disconnect $PLUG_SNAP:shared-content-plug || : + # Ensure there is no preserved mount namespace of the -plug snap. + # (This one gets created because by connect hooks). + . "$TESTSLIB/dirs.sh" + $LIBEXECDIR/snapd/snap-discard-ns $PLUG_SNAP + rm -f /run/snapd/ns/$PLUG_SNAP.mnt +execute: | + # NOTE: All the commands here will focus on the -plug snap as this is where + # the mount namespace is going to be altered. The -slot snap is just there + # inert, as a way to provide content, but it does not execute and does not + # need a namespace namespace. + + . "$TESTSLIB/dirs.sh" + + # Check that update tool doesn't fail if there is no namespace yet. + $LIBEXECDIR/snapd/snap-update-ns $PLUG_SNAP + + # Run a trivial command to build and preserve a mount namespace. + snap run --shell $PLUG_SNAP.content-plug -c 'true' + + # Check that the shared content is not mounted. + snap run --shell $PLUG_SNAP.content-plug -c 'test ! -e $SNAP/import/shared-content' + + # Run snap-update-ns to see that we managed to switch namespaces correctly + # and did nothing more. We did nothing more because the namespace already + # is exactly as it needs to be. The snap-confine program has just + # constructed it according to the desired description. + diff -Nu /var/lib/snapd/mount/snap.$PLUG_SNAP.fstab /run/snapd/ns/snap.$PLUG_SNAP.fstab + $LIBEXECDIR/snapd/snap-update-ns $PLUG_SNAP + diff -Nu /var/lib/snapd/mount/snap.$PLUG_SNAP.fstab /run/snapd/ns/snap.$PLUG_SNAP.fstab + + # Connect the plug to the slot. + snap connect $PLUG_SNAP:shared-content-plug $SLOT_SNAP:shared-content-slot + + # Run the update tool manually to see that it is idempotent. + diff -Nu /var/lib/snapd/mount/snap.$PLUG_SNAP.fstab /run/snapd/ns/snap.$PLUG_SNAP.fstab + $LIBEXECDIR/snapd/snap-update-ns $PLUG_SNAP + diff -Nu /var/lib/snapd/mount/snap.$PLUG_SNAP.fstab /run/snapd/ns/snap.$PLUG_SNAP.fstab + + # Check that the shared content is mounted. + snap run --shell $PLUG_SNAP.content-plug -c 'test -e $SNAP/import/shared-content' + + # Disconnect the plug from the slot so that we can test the other way. + snap disconnect $PLUG_SNAP:shared-content-plug $SLOT_SNAP:shared-content-slot + + # Run the update tool manually to see that it is idempotent. + diff -uN /var/lib/snapd/mount/snap.$PLUG_SNAP.fstab /run/snapd/ns/snap.$PLUG_SNAP.fstab + $LIBEXECDIR/snapd/snap-update-ns $PLUG_SNAP + diff -uN /var/lib/snapd/mount/snap.$PLUG_SNAP.fstab /run/snapd/ns/snap.$PLUG_SNAP.fstab + + # Check that the shared content is not mounted. + snap run --shell $PLUG_SNAP.content-plug -c 'test ! -e $SNAP/import/shared-content' + + # Discard the namespace so that update has nothing useful to do. + $LIBEXECDIR/snapd/snap-discard-ns $PLUG_SNAP + $LIBEXECDIR/snapd/snap-update-ns $PLUG_SNAP diff --git a/tests/main/snap-userd-reexec/task.yaml b/tests/main/snap-userd-reexec/task.yaml new file mode 100644 index 00000000..a19a13eb --- /dev/null +++ b/tests/main/snap-userd-reexec/task.yaml @@ -0,0 +1,17 @@ +summary: Check that core refresh will create the userd dbus serivce file + +# only run on systems that re-exec +systems: [ubuntu-16*, ubuntu-17*] + +execute: | + snap list | awk "/^core / {print(\$3)}" > prevBoot + + echo "Ensure service file is created if missing (e.g. on re-exec)" + mv /usr/share/dbus-1/services/io.snapcraft.Launcher.service /usr/share/dbus-1/services/io.snapcraft.Launcher.service.orig + + echo "Install new core" + snap install --dangerous /var/lib/snapd/snaps/core_$(cat prevBoot).snap + + echo "Ensure the dbus service file got created" + test -f /usr/share/dbus-1/services/io.snapcraft.Launcher.service + diff -u /usr/share/dbus-1/services/io.snapcraft.Launcher.service.orig /usr/share/dbus-1/services/io.snapcraft.Launcher.service \ No newline at end of file diff --git a/tests/main/snap-userd/task.yaml b/tests/main/snap-userd/task.yaml new file mode 100644 index 00000000..07d235dd --- /dev/null +++ b/tests/main/snap-userd/task.yaml @@ -0,0 +1,65 @@ +summary: Ensure snap userd allows opening a URL via xdg-open + +systems: + # Not supposed to work on Ubuntu Core systems as we don't have + # a user session environment there + - -ubuntu-core-* + +environment: + DISPLAY: :0 + +restore: | + . "$TESTSLIB/dirs.sh" + . "$TESTSLIB/pkgdb.sh" + rm -f dbus.env + umount -f /usr/bin/xdg-open || true + +execute: | + . "$TESTSLIB/pkgdb.sh" + . "$TESTSLIB/dirs.sh" + + dbus-launch > dbus.env + export $(cat dbus.env | xargs) + + # wait for session to be ready + ping_launcher() { + dbus-send --session \ + --dest=io.snapcraft.Launcher \ + --type=method_call \ + --print-reply \ + / \ + org.freedesktop.DBus.Peer.Ping + } + while ! ping_launcher ; do + sleep .5 + done + + # Create a small helper which will tell us if snap passes + # the URL correctly to the right handler + cat << 'EOF' > /tmp/xdg-open + #!/bin/sh + echo "$@" > /tmp/xdg-open-output + EOF + chmod +x /tmp/xdg-open + touch /usr/bin/xdg-open + mount --bind /tmp/xdg-open /usr/bin/xdg-open + + ensure_xdg_open_output() { + rm -f /tmp/xdg-open-output + export DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS + $SNAP_MOUNT_DIR/core/current/usr/bin/xdg-open $1 + test -e /tmp/xdg-open-output + test "$(cat /tmp/xdg-open-output)" = $1 + } + + # Ensure http, https and mailto work + ensure_xdg_open_output "https://snapcraft.io" + ensure_xdg_open_output "http://snapcraft.io" + ensure_xdg_open_output "mailto:talk@snapcraft.io" + + # Ensure other schemes are not passed through + rm /tmp/xdg-open-output + ! $SNAP_MOUNT_DIR/core/current/usr/bin/xdg-open ftp://snapcraft.io + test ! -e /tmp/xdg-open-output + ! $SNAP_MOUNT_DIR/core/current/usr/bin/xdg-open aabbcc + test ! -e /tmp/xdg-open-output diff --git a/tests/main/snapctl-configure-core/task.yaml b/tests/main/snapctl-configure-core/task.yaml new file mode 100644 index 00000000..7ffb6d60 --- /dev/null +++ b/tests/main/snapctl-configure-core/task.yaml @@ -0,0 +1,67 @@ +summary: Ensure "snapctl core-configure" works + +# the test is only meaningful on core devices +systems: [ubuntu-core-*] + +prepare: | + cat > new-configure-core < /var/lib/snapd/state.json.new + mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json + systemctl start snapd.{service,socket} + + echo "Verify that cookie file was re-created" + check_cookie snapctl-from-snap + + echo "Verify that snapctl get can be executed by the app and shows the value set by configure hook" + $SNAP_MOUNT_DIR/bin/snapctl-from-snap.snapctl-get foo | MATCH bar + + echo "Verify that snapctl set can modify configuration values" + $SNAP_MOUNT_DIR/bin/snapctl-from-snap.snapctl-set foo=123 + $SNAP_MOUNT_DIR/bin/snapctl-from-snap.snapctl-get foo | MATCH 123 + + echo "Verify configuration value with snap get" + snap get snapctl-from-snap foo | MATCH 123 + + echo "Given two revisions of a snap have been installed" + snap install --dangerous snapctl-from-snap_1.0_all.snap + check_cookie snapctl-from-snap + + echo "And a single revision gets removed" + snap remove snapctl-from-snap --revision=x1 + + echo "Verify that cookie file is still present" + check_cookie snapctl-from-snap + + echo "Verify that cookie is not removed when snap is disabled" + snap disable snapctl-from-snap + check_cookie snapctl-from-snap + snap enable snapctl-from-snap + check_cookie snapctl-from-snap + + echo "Verify that snap cookie is removed on snap removal" + snap remove snapctl-from-snap + if test -f $COOKIE_FILE ; then + echo "Cookie file $COOKIE_FILE still exists" + exit 1 + fi diff --git a/tests/main/snapctl-services/task.yaml b/tests/main/snapctl-services/task.yaml new file mode 100644 index 00000000..9ac04691 --- /dev/null +++ b/tests/main/snapctl-services/task.yaml @@ -0,0 +1,78 @@ +summary: Check that own services can be controlled by snapctl + +kill-timeout: 3m +environment: + SERVICEOPTIONFILE: /var/snap/test-snapd-service/current/service-option + +restore: | + rm -f $SERVICEOPTIONFILE + +execute: | + wait_for_service() { + retry=5 + while ! snap services $1 | MATCH $2; do + retry=$(( retry - 1 )) + if [ $retry -le 0 ]; then + echo "Failed to match the status of service $1, expected: $2" + exit 1 + fi + sleep 1 + done + } + + wait_for_file_change() { + retry=5 + while ! cat $1 | MATCH $2; do + retry=$(( retry - 1 )) + if [ $retry -le 0 ]; then + echo "Failed to match the content of file $1, expected: $2" + exit 1 + fi + sleep 1 + done + } + + echo "When the service snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-service + + echo "We can see it running" + wait_for_service "test-snapd-service.test-snapd-service" " active" + + echo "When we stop the service via configure hook" + snap set test-snapd-service command=stop + + echo "It's stopped" + wait_for_service "test-snapd-service.test-snapd-service" " inactive" + + echo "When we start the service via configure hook" + snap set test-snapd-service command=start + + echo "It's running again" + wait_for_service "test-snapd-service.test-snapd-service" " active" + + echo "When we stop it again" + snap set test-snapd-service command=stop + + echo "It's stopped" + wait_for_service "test-snapd-service.test-snapd-service" " inactive" + + echo "And then restart" + snap set test-snapd-service service-option-source=foo command=restart + + echo "It's running" + wait_for_service "test-snapd-service.test-snapd-service" " active" + + echo "And restart command was executed as part of configure hook change" + snap tasks --last=configure|MATCH -z "restart of .test-snapd-service.test-snapd-service.+restart of .test-snapd-service.test-snapd-other-service" + + echo "And service could get the new service-option set from the hook" + wait_for_file_change $SERVICEOPTIONFILE "^foo$" + + echo "Reinstalling the snap with configure hook calling snapctl restart works" + snap set test-snapd-service command=restart + install_local test-snapd-service + if journalctl|grep -q "error running snapctl"; then + echo "snapctl should not report errors" + exit 1 + fi diff --git a/tests/main/snapctl/task.yaml b/tests/main/snapctl/task.yaml new file mode 100644 index 00000000..ee36e8e1 --- /dev/null +++ b/tests/main/snapctl/task.yaml @@ -0,0 +1,27 @@ +summary: Check that `snapctl` can be run from within hooks + +prepare: | + snap pack $TESTSLIB/snaps/snapctl-hooks + snap install --dangerous snapctl-hooks_1.0_all.snap + +restore: | + rm -f snapctl-hooks_1.0_all.snap + +execute: | + echo "Verify that snapctl -h runs without a context" + if ! snapctl -h; then + echo "Expected snapctl -h to be successful" + exit 1 + fi + + echo "Verify that the snapd API is only available via the snapd socket" + if ! printf "GET /v2/snaps HTTP/1.0\r\n\r\n" | nc -U -w 1 /run/snapd.socket | grep "200 OK"; then + echo "Expected snapd API to be available on the snapd socket" + echo "Got: $(curl -s --unix-socket /run/snapd.socket http:/v2/snaps)" + exit 1 + fi + + if ! printf "GET /v2/snaps HTTP/1.0\r\n\r\n" | nc -U -w 1 /run/snapd-snap.socket | grep "401 Unauthorized"; then + echo "Expected snapd API to be unauthorized on the snap socket" + exit 1 + fi diff --git a/tests/main/snapd-notify/task.yaml b/tests/main/snapd-notify/task.yaml new file mode 100644 index 00000000..46299aa4 --- /dev/null +++ b/tests/main/snapd-notify/task.yaml @@ -0,0 +1,17 @@ +summary: Ensure snapd notify feature is working + +# this test requires SNAPD_DEBUG to be set, we can't make that assumption for the +# external backend +backends: [-external] + +execute: | + for _ in $(seq 5); do + if systemctl status snapd.service | MATCH "Active: active"; then + journalctl -u snapd | MATCH "activation done in" + exit + fi + sleep 1 + done + + echo "Snapd service status not active" + exit 1 diff --git a/tests/main/snapd-reexec/task.yaml b/tests/main/snapd-reexec/task.yaml new file mode 100644 index 00000000..8d579c52 --- /dev/null +++ b/tests/main/snapd-reexec/task.yaml @@ -0,0 +1,94 @@ +summary: Test that snapd reexecs itself into core + +# Disable for Fedora, openSUSE as re-exec is not support there yet +systems: [-ubuntu-core-16-*, -fedora-*, -opensuse-*] + +restore: | + . $TESTSLIB/dirs.sh + # extra cleanup in case something in this test went wrong + rm -f /etc/systemd/system/snapd.service.d/no-reexec.conf + systemctl stop snapd.service snapd.socket + if mount|grep "/snap/core/.*/usr/lib/snapd/info"; then + umount $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/info + fi + if mount|grep "/snap/core/.*/usr/lib/snapd/snapd"; then + umount $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snapd + fi + rm -f /tmp/old-info + +debug: | + ls /etc/systemd/system/snapd.service.d + cat /etc/systemd/system/snapd.service.d/* + +execute: | + if [ "${SNAP_REEXEC:-}" = "0" ]; then + echo "skipping test when SNAP_REEXEC is disabled" + exit 0 + fi + + echo "Ensure we re-exec by default" + /usr/bin/env SNAPD_DEBUG=1 snap list 2>&1 | MATCH "DEBUG: restarting into" + + . $TESTSLIB/dirs.sh + + echo "Ensure that we do not re-exec into older versions" + systemctl stop snapd.service snapd.socket + echo "mount something older than our freshly build snapd" + echo "VERSION=1.0">/tmp/old-info + mount --bind /tmp/old-info $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/info + systemctl start snapd.service snapd.socket + snap list + journalctl | MATCH "core snap \(at .*\) is older \(.*\) than distribution package" + + echo "Revert back to normal" + systemctl stop snapd.service snapd.socket + umount $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/info + + echo "Ensure SNAP_REEXEC=0 is honored for snapd" + cat > /etc/systemd/system/snapd.service.d/reexec.conf < /tmp/broken-snapd < $SERVICE_FILE + chmod a+x $SERVICE_FILE + systemd_create_and_start_unit $SERVICE_NAME "$(readlink -f $SERVICE_FILE)" + + while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done + +restore: | + . $TESTSLIB/systemd.sh + systemd_stop_and_destroy_unit $SERVICE_NAME + rm -f $SERVICE_FILE $READABLE_FILE + +execute: | + echo "Given a buildable snap in a known directory" + echo "When try is executed on that directory" + snap try $TESTSLIB/snaps/test-snapd-tools + + echo "Then the snap is listed as installed with try in the notes" + snap list | MATCH '^test-snapd-tools .* try' + + echo "And commands from the snap-try binary can be run" + test-snapd-tools.success + + echo "And commands from the snap-try binary can read in a readable dir" + echo -n "Hello World" > $READABLE_FILE + test-snapd-tools.cat $READABLE_FILE | MATCH "Hello World" + + echo "=====================================" + + echo "Given a buildable snap which access confinement-protected resources in a known directory" + echo "When try is executed on that directory" + snap try $TESTSLIB/snaps/test-snapd-tools + + if [ "$(snap debug confinement)" = strict ] ; then + echo "Then the snap command is not able to access the protected resource" + if test-snapd-tools.head -1 /dev/kmsg; then + echo "Expected confinement denial in try mode didn't work" + exit 1 + fi + fi + + echo "=====================================" + + echo "Given a buildable snap which access confinement-protected resources in a known directory" + echo "When try is executed on that directory with devmode enabled" + snap try $TESTSLIB/snaps/test-snapd-tools --devmode + + echo "Then the snap command is able to access the protected resource" + test-snapd-tools.head -1 /dev/kmsg + + echo "=====================================" + + echo "Given a buildable snap which access confinement-enabled network resources in a known directory" + echo "When try is executed on that directory" + snap try $TESTSLIB/snaps/network-consumer + + echo "Then the snap is able to access the network resource" + network-consumer http://127.0.0.1:$PORT | MATCH "ok" + + echo "=====================================" + n=test-snapd-classic-confinement + s=$TESTSLIB/snaps/$n + echo "Given a buildable snap with classic confinement:" + echo " - you can't try it without --classic:" + snap try $s && exit 1 || true + snap try --jailmode $s && exit 1 || true + snap try --devmode $s && exit 1 || true + + touch /tmp/lala + + echo " - you can try it with --classic:" + snap try --classic $s + snap list $n | MATCH $n.*classic + $n | MATCH lala + snap remove $n + + # SOON: + # echo " - you can also try it with --classic --jailmode:" + # snap try --classic --devmode $s + # snap list $n | MATCH "$n.*(classic,jailmode|jailmode,classic)" + # $n | MATCH lala diff --git a/tests/main/ubuntu-core-apt/task.yaml b/tests/main/ubuntu-core-apt/task.yaml new file mode 100644 index 00000000..b08b9814 --- /dev/null +++ b/tests/main/ubuntu-core-apt/task.yaml @@ -0,0 +1,9 @@ +summary: Ensure that the apt output on ubuntu-core is correct +systems: [ubuntu-core-16-*] +execute: | + expected="Ubuntu Core does not use apt-get, see 'snap --help'!" + output=$(apt-get update) + if [ "$output" != "$expected" ]; then + echo "Unexpected apt output: $output" + exit 1 + fi diff --git a/tests/main/ubuntu-core-classic/task.yaml b/tests/main/ubuntu-core-classic/task.yaml new file mode 100644 index 00000000..c9d23eb4 --- /dev/null +++ b/tests/main/ubuntu-core-classic/task.yaml @@ -0,0 +1,49 @@ +summary: Ensure classic dimension works correctly +systems: [ubuntu-core-16-*] +environment: + # We need to set the SUDO_USER here to simulate the real + # behavior. I.e. when entering classic it happens via + # `sudo classic` and the user gets a user shell inside + # the classic environment that has sudo support. + SUDO_USER: test +prepare: | + echo "test ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/create-test +restore: | + rm -f /etc/sudoers.d/create-test +execute: | + echo "Ensure classic can be installed" + snap install --devmode --beta classic + snap list|MATCH classic + + echo "Check that classic can run commands inside classic" + classic test -f /var/lib/dpkg/status + + echo "Ensure that after classic exits no processes are left behind" + classic "sleep 133713371337&" + # use "[f]oo" to search for "foo" in ps output without having to filter yourself out + if ps afx|grep '[1]33713371337'; then + echo "The sleep process was not killed when classic exited" + echo "Something is wrong with the cleanup" + exit 1 + fi + + echo "Ensure sudo works without a password inside classic" + # classic uses "script" to work around the issue that + # tty reports "no tty" inside snaps (LP: #1611493) + # + # "script" adds extra \r into the output that we need to filter here + if [ "$(classic sudo id -u|tr -d "\r")" != "0" ]; then + echo "sudo inside classic did not work as expected" + exit 1 + fi + + for d in /proc /run /sys /dev /snappy; do + if ! classic test -d $d; then + echo "Expected dir $d is missing inside classic" + exit 1 + fi + if ! classic mount | MATCH "$d"; then + echo "Expected bind mount for $d in classic missing" + exit 1 + fi + done diff --git a/tests/main/ubuntu-core-create-user/task.yaml b/tests/main/ubuntu-core-create-user/task.yaml new file mode 100644 index 00000000..f1f939e8 --- /dev/null +++ b/tests/main/ubuntu-core-create-user/task.yaml @@ -0,0 +1,38 @@ +summary: Ensure that snap create-user works in ubuntu-core +systems: [ubuntu-core-16-*] +restore: | + # meh, deluser has no --extrausers support + sed -i '/^mvo/d' /var/lib/extrausers/passwd + sed -i '/^mvo/d' /var/lib/extrausers/shadow + sed -i '/^mvo/d' /var/lib/extrausers/group + rm -rf /home/mvo + rm -f create.error +execute: | + if [ "$MANAGED_DEVICE" = "true" ]; then + if snap create-user --sudoer mvo@ubuntu.com 2>create.error; then + echo "Did not get expected error creating user in managed device" + exit 1 + fi + MATCH "cannot create user: device already managed" < create.error + exit 0 + fi + echo "Adding invalid user" + expected='error: while creating user: cannot create user "nosuchuser@example.com"' + if output=$(snap create-user nosuchuser@example.com 2>&1); then + echo "snap create-user should fail for unknown users but it did not" + exit 1 + fi + MATCH "$expected" <<<"$output" + + echo "Adding valid user" + expected='created user "mvo"' + output=$(snap create-user --sudoer mvo@ubuntu.com) + if [ "$output" != "$expected" ]; then + echo "Unexpected output $output" + exit 1 + fi + echo "Ensure there are ssh keys imported" + MATCH ssh-rsa < /home/mvo/.ssh/authorized_keys + + echo "Ensure the user is a sudo user" + sudo -u mvo sudo true diff --git a/tests/main/ubuntu-core-custom-device-reg-extras/manip_seed.py b/tests/main/ubuntu-core-custom-device-reg-extras/manip_seed.py new file mode 100644 index 00000000..35251a91 --- /dev/null +++ b/tests/main/ubuntu-core-custom-device-reg-extras/manip_seed.py @@ -0,0 +1,21 @@ +import sys +import yaml + +with open(sys.argv[1]) as f: + seed = yaml.load(f) + +i = 0 +snaps = seed['snaps'] +while i < len(snaps): + entry = snaps[i] + if entry['name'] == 'pc': + snaps[i] = { + "name": "pc", + "unasserted": True, + "file": "pc_x1.snap", + } + break + i += 1 + +with open(sys.argv[1], 'w') as f: + yaml.dump(seed, stream=f, indent=2, default_flow_style=False) diff --git a/tests/main/ubuntu-core-custom-device-reg-extras/prepare-device b/tests/main/ubuntu-core-custom-device-reg-extras/prepare-device new file mode 100755 index 00000000..bcee1572 --- /dev/null +++ b/tests/main/ubuntu-core-custom-device-reg-extras/prepare-device @@ -0,0 +1,5 @@ +#!/bin/sh +snapctl set device-service.url=http://localhost:11029 +snapctl set device-service.headers='{"X-Use-Proposed": "yes"}' +snapctl set registration.proposed-serial="Y1234" +snapctl set registration.body='mac: "00:00:00:00:ff:00"' diff --git a/tests/main/ubuntu-core-custom-device-reg-extras/task.yaml b/tests/main/ubuntu-core-custom-device-reg-extras/task.yaml new file mode 100644 index 00000000..a4e00ca3 --- /dev/null +++ b/tests/main/ubuntu-core-custom-device-reg-extras/task.yaml @@ -0,0 +1,81 @@ +summary: | + Test that device initialisation and registration can be customized + with the prepare-device gadget hook and this can set request headers, + a proposed serial and the body of the serial assertion +systems: [ubuntu-core-16-64] +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/systemd.sh + systemctl stop snapd.service snapd.socket + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + unsquashfs /var/lib/snapd/snaps/pc_*.snap + mkdir -p squashfs-root/meta/hooks + cp prepare-device squashfs-root/meta/hooks + mksquashfs squashfs-root pc_x1.snap -comp xz + rm -rf squashfs-root + cp pc_x1.snap /var/lib/snapd/seed/snaps/ + mv /var/lib/snapd/seed/assertions/model model.bak + cp /var/lib/snapd/seed/seed.yaml seed.yaml.bak + python3 ./manip_seed.py /var/lib/snapd/seed/seed.yaml + cp $TESTSLIB/assertions/developer1.account /var/lib/snapd/seed/assertions + cp $TESTSLIB/assertions/developer1.account-key /var/lib/snapd/seed/assertions + cp $TESTSLIB/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + cp $TESTSLIB/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions + # start fake device svc + systemd_create_and_start_unit fakedevicesvc "$(which fakedevicesvc) localhost:11029" + # kick first boot again + systemctl start snapd.service snapd.socket +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/systemd.sh + systemctl stop snapd.service snapd.socket + systemd_stop_and_destroy_unit fakedevicesvc + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + if systemctl status snap-pc-x1.mount ; then + systemctl stop snap-pc-x1.mount + rm -f /etc/systemd/system/snap-pc-x1.mount + rm -f /etc/systemd/system/multi-user.target.wants/snap-pc-x1.mount + rm -f /var/lib/snapd/snaps/pc_x1.snap + systemctl daemon-reload + fi + rm -f /var/lib/snapd/seed/snaps/pc_x1.snap + cp seed.yaml.bak /var/lib/snapd/seed/seed.yaml + rm -f /var/lib/snapd/seed/assertions/developer1.account + rm -f /var/lib/snapd/seed/assertions/developer1.account-key + rm -f /var/lib/snapd/seed/assertions/developer1-pc.model + rm -f /var/lib/snapd/seed/assertions/testrootorg-store.account-key + cp model.bak /var/lib/snapd/seed/assertions/model + rm -f *.bak + # kick first boot again + systemctl start snapd.service snapd.socket + # wait for first boot to be done + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "Wait for first boot to be done" + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done + echo "We have a model assertion" + snap known model|MATCH "model: my-model" + + echo "Wait for device initialisation to be done" + while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done + + echo "Check we have a serial" + snap known serial|MATCH "authority-id: developer1" + snap known serial|MATCH "brand-id: developer1" + snap known serial|MATCH "model: my-model" + snap known serial|MATCH "serial: Y1234" + snap known serial|MATCH 'mac: "00:00:00:00:ff:00"' diff --git a/tests/main/ubuntu-core-custom-device-reg/manip_seed.py b/tests/main/ubuntu-core-custom-device-reg/manip_seed.py new file mode 100644 index 00000000..35251a91 --- /dev/null +++ b/tests/main/ubuntu-core-custom-device-reg/manip_seed.py @@ -0,0 +1,21 @@ +import sys +import yaml + +with open(sys.argv[1]) as f: + seed = yaml.load(f) + +i = 0 +snaps = seed['snaps'] +while i < len(snaps): + entry = snaps[i] + if entry['name'] == 'pc': + snaps[i] = { + "name": "pc", + "unasserted": True, + "file": "pc_x1.snap", + } + break + i += 1 + +with open(sys.argv[1], 'w') as f: + yaml.dump(seed, stream=f, indent=2, default_flow_style=False) diff --git a/tests/main/ubuntu-core-custom-device-reg/prepare-device b/tests/main/ubuntu-core-custom-device-reg/prepare-device new file mode 100755 index 00000000..ac974b4d --- /dev/null +++ b/tests/main/ubuntu-core-custom-device-reg/prepare-device @@ -0,0 +1,2 @@ +#!/bin/sh +snapctl set device-service.url=http://localhost:11029 diff --git a/tests/main/ubuntu-core-custom-device-reg/task.yaml b/tests/main/ubuntu-core-custom-device-reg/task.yaml new file mode 100644 index 00000000..7d0154a6 --- /dev/null +++ b/tests/main/ubuntu-core-custom-device-reg/task.yaml @@ -0,0 +1,79 @@ +summary: | + Test that device initialisation and registration can be customized + with the prepare-device gadget hook +systems: [ubuntu-core-16-64] +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/systemd.sh + systemctl stop snapd.service snapd.socket + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + unsquashfs /var/lib/snapd/snaps/pc_*.snap + mkdir -p squashfs-root/meta/hooks + cp prepare-device squashfs-root/meta/hooks + mksquashfs squashfs-root pc_x1.snap -comp xz + rm -rf squashfs-root + cp pc_x1.snap /var/lib/snapd/seed/snaps/ + mv /var/lib/snapd/seed/assertions/model model.bak + cp /var/lib/snapd/seed/seed.yaml seed.yaml.bak + python3 ./manip_seed.py /var/lib/snapd/seed/seed.yaml + cp $TESTSLIB/assertions/developer1.account /var/lib/snapd/seed/assertions + cp $TESTSLIB/assertions/developer1.account-key /var/lib/snapd/seed/assertions + cp $TESTSLIB/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + cp $TESTSLIB/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions + # start fake device svc + systemd_create_and_start_unit fakedevicesvc "$(which fakedevicesvc) localhost:11029" + # kick first boot again + systemctl start snapd.service snapd.socket +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/systemd.sh + systemctl stop snapd.service snapd.socket + systemd_stop_and_destroy_unit fakedevicesvc + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + if systemctl status snap-pc-x1.mount ; then + systemctl stop snap-pc-x1.mount + rm -f /etc/systemd/system/snap-pc-x1.mount + rm -f /etc/systemd/system/multi-user.target.wants/snap-pc-x1.mount + rm -f /var/lib/snapd/snaps/pc_x1.snap + systemctl daemon-reload + fi + rm -f /var/lib/snapd/seed/snaps/pc_x1.snap + cp seed.yaml.bak /var/lib/snapd/seed/seed.yaml + rm -f /var/lib/snapd/seed/assertions/developer1.account + rm -f /var/lib/snapd/seed/assertions/developer1.account-key + rm -f /var/lib/snapd/seed/assertions/developer1-pc.model + rm -f /var/lib/snapd/seed/assertions/testrootorg-store.account-key + cp model.bak /var/lib/snapd/seed/assertions/model + rm -f *.bak + # kick first boot again + systemctl start snapd.service snapd.socket + # wait for first boot to be done + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + echo "Wait for first boot to be done" + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done + echo "We have a model assertion" + snap known model|MATCH "model: my-model" + + echo "Wait for device initialisation to be done" + while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done + + echo "Check we have a serial" + snap known serial|MATCH "authority-id: developer1" + snap known serial|MATCH "brand-id: developer1" + snap known serial|MATCH "model: my-model" + snap known serial|MATCH "serial: 7777" diff --git a/tests/main/ubuntu-core-device-reg/task.yaml b/tests/main/ubuntu-core-device-reg/task.yaml new file mode 100644 index 00000000..9cfc24aa --- /dev/null +++ b/tests/main/ubuntu-core-device-reg/task.yaml @@ -0,0 +1,28 @@ +summary: | + Ensure after device initialisation registration worked and + we have a serial and can acquire a session macaroon +systems: [ubuntu-core-16-*] +execute: | + echo "Wait for first boot to be done" + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done + echo "We have a model assertion" + snap known model|MATCH "series: 16" + + if ! snap known model|grep "brand-id: canonical" ; then + echo "Not a canonical model. Skipping." + exit 0 + fi + + echo "Wait for device initialisation to be done" + while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done + + echo "Check we have a serial" + snap known serial|MATCH "authority-id: canonical" + snap known serial|MATCH "brand-id: canonical" + if [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then + snap known serial|MATCH "model: pc" + fi + + echo "Make sure we could acquire a session macaroon" + snap find pc + MATCH '"session-macaroon":"[^"]' < /var/lib/snapd/state.json diff --git a/tests/main/ubuntu-core-fan/task.yaml b/tests/main/ubuntu-core-fan/task.yaml new file mode 100644 index 00000000..1634bd5b --- /dev/null +++ b/tests/main/ubuntu-core-fan/task.yaml @@ -0,0 +1,16 @@ +summary: Test ubuntu-fan +systems: [ubuntu-core-16-*] +prepare: | + IP=$(ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | cut -d' ' -f1|head -1) + fanctl up 241.0.0.0/8 $IP/16 +restore: | + fanctl down -e +execute: | + echo "Test that fanctl exists" + command -v fanctl + + echo "Test fanctl created fan bridge" + ifconfig |MATCH ^fan-241 + + # FIXME: port the docker tests once we have docker again + # https://github.com/snapcore/snapd/blob/2.13/integration-tests/tests/ubuntufan_test.go#L88 diff --git a/tests/main/ubuntu-core-gadget-config-defaults/manip_seed.py b/tests/main/ubuntu-core-gadget-config-defaults/manip_seed.py new file mode 100644 index 00000000..bb591fab --- /dev/null +++ b/tests/main/ubuntu-core-gadget-config-defaults/manip_seed.py @@ -0,0 +1,27 @@ +import sys +import yaml + +with open(sys.argv[1]) as f: + seed = yaml.load(f) + +i = 0 +snaps = seed['snaps'] +while i < len(snaps): + entry = snaps[i] + if entry['name'] == 'pc': + snaps[i] = { + "name": "pc", + "unasserted": True, + "file": "pc_x1.snap", + } + break + i += 1 + +snaps.append({ + "name": "test-snapd-with-configure", + "channel": "edge", + "file": sys.argv[2], +}) + +with open(sys.argv[1], 'w') as f: + yaml.dump(seed, stream=f, indent=2, default_flow_style=False) diff --git a/tests/main/ubuntu-core-gadget-config-defaults/task.yaml b/tests/main/ubuntu-core-gadget-config-defaults/task.yaml new file mode 100644 index 00000000..5c8e93a5 --- /dev/null +++ b/tests/main/ubuntu-core-gadget-config-defaults/task.yaml @@ -0,0 +1,102 @@ +summary: | + Test that config defaults specified in the gadget are picked up + for first boot snaps +systems: [ubuntu-core-16-64] +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + . $TESTSLIB/systemd.sh + systemctl stop snapd.service snapd.socket + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + snap download --edge test-snapd-with-configure + unsquashfs /var/lib/snapd/snaps/pc_*.snap + # fill in defaults + TEST_SNAP_ID= + if [ "$SNAPPY_USE_STAGING_STORE" = 1 ]; then + TEST_SNAP_ID=jHxWQxtGqu7tHwiq7F8Ojk5qazcEeslT + else + TEST_SNAP_ID=aLcJorEJZgJNUGL2GMb3WR9SoVyHUNAd + fi + cat >> squashfs-root/meta/gadget.yaml < nextBoot + snap install test-snapd-tools + +execute: | + . $TESTSLIB/boot.sh + + # FIXME Why it starting with snap_mode=try the first time? + # Perhaps because core is installed after seeding? Do we + # want that on pristine images? + if [ $SPREAD_REBOOT != 0 ]; then + echo "Waiting for snapd to clean snap_mode" + while [ "$(bootenv snap_mode)" != "" ]; do + sleep 1 + done + + echo "Ensure the bootloader is correct after reboot" + test "$(bootenv snap_core)" = "core_$(cat nextBoot).snap" + test "$(bootenv snap_try_core)" = "" + test "$(bootenv snap_mode)" = "" + fi + + snap list | awk "/^core / {print(\$3)}" > prevBoot + + # wait for ongoing change if there is one + if [ -f curChg ] ; then + snap watch $(cat curChg) + rm -f curChg + fi + + case $SPREAD_REBOOT in + + 0) cmd="snap install --dangerous /var/lib/snapd/snaps/core_$(cat prevBoot).snap" ;; + 1) cmd="snap revert core" ;; + 2) cmd="snap install --dangerous /var/lib/snapd/snaps/core_$(cat prevBoot).snap" ;; + 3) cmd="snap revert core" ;; + 4) exit 0 ;; + + esac + + # start the op and get the change id + chg_id=$(eval ${cmd} --no-wait) + + # save change id to wait later or abort + echo ${chg_id} >curChg + + # wait for the link task to be done + while ! snap change ${chg_id}|grep -q "^Done.*Make snap.*available to the system" ; do sleep 1 ; done + + echo "Ensure the test snap still runs" + test-snapd-tools.echo hello | MATCH hello + + echo "Ensure the bootloader is correct before reboot" + snap list | awk "/^core / {print(\$3)}" > nextBoot + test "$(cat prevBoot)" != "$(cat nextBoot)" + test "$(bootenv snap_try_core)" = "core_$(cat nextBoot).snap" + test "$(bootenv snap_mode)" = "try" + + echo "Ensure the device is scheduled for auto-reboot" + output=$(dbus-send --print-reply \ + --type=method_call \ + --system \ + --dest=org.freedesktop.login1 \ + /org/freedesktop/login1 \ + org.freedesktop.DBus.Properties.Get \ + string:org.freedesktop.login1.Manager string:ScheduledShutdown) + if ! echo $output | MATCH 'string "reboot"'; then + echo "Failed to detect scheduled reboot in logind output" + exit 1 + fi + + REBOOT diff --git a/tests/main/ubuntu-core-writablepaths/task.yaml b/tests/main/ubuntu-core-writablepaths/task.yaml new file mode 100644 index 00000000..191dd227 --- /dev/null +++ b/tests/main/ubuntu-core-writablepaths/task.yaml @@ -0,0 +1,38 @@ +summary: Ensure that the writable paths on the image are correct +systems: [ubuntu-core-16-*] +execute: | + echo "Ensure everything in writable-paths is actually writable" + cat /etc/system-image/writable-paths | while read -r line; do + line=$(echo $line | sed -e '/\s*#.*$/d') + if [ -z "$line" ]; then + continue; + fi + # a writable-path may be either a file or a directory + dir_or_file=$(echo $line|cut -f1 -d' ') + if [ ! -e "$dir_or_file" ]; then + echo "$dir_or_file" >> missing + elif [ -f "$dir_or_file" ]; then + if ! touch "$dir_or_file"; then + echo "$dir_or_file" >> broken + fi + elif ! touch "$dir_or_file"/random-name-that-I-made-up; then + echo "$dir_or_file" >> broken + fi + rm -f $dir_or_file/random-name-that-I-made-up + done + + if [ -s "broken" ]; then + echo "The following writable paths are not writable:" + cat broken + fi + if [ -s "missing" ]; then + echo "The following writable paths are missing:" + cat missing + fi + # FIMXE: make missing fatal as well + #if [ -s missing ] || [ -s broken ]; then + # exit 1 + #fi + if [ -s broken ]; then + exit 1 + fi diff --git a/tests/main/user-data-handling/task.yaml b/tests/main/user-data-handling/task.yaml new file mode 100644 index 00000000..6b849fe4 --- /dev/null +++ b/tests/main/user-data-handling/task.yaml @@ -0,0 +1,27 @@ +summary: Check that the refresh data copy works. + +execute: | + echo "For an installed snap" + snap install test-snapd-tools + rev=$(snap list|grep test-snapd-tools|tr -s ' '|cut -f3 -d' ') + + echo "That has some user data" + mkdir -p /home/*/snap/test-snapd-tools/$rev/ + touch /home/*/snap/test-snapd-tools/$rev/mock-data + mkdir -p /root/snap/test-snapd-tools/$rev/ + touch /root/snap/test-snapd-tools/$rev/mock-data + + echo "When the snap is refreshed" + snap refresh --channel=edge test-snapd-tools + new_rev=$(snap list|grep test-snapd-tools|tr -s ' '|cut -f3 -d' ') + + echo "Then the user data gets copied" + test -e /home/*/snap/test-snapd-tools/$new_rev/mock-data + test -e /root/snap/test-snapd-tools/$new_rev/mock-data + + echo "When the snap is removed" + snap remove test-snapd-tools + + echo "Then all user data and root data is gone" + ! test -e /home/*/snap/test-snapd-tools/$new_rev/mock-data + ! test -e /root/snap/test-snapd-tools/$new_rev/mock-data diff --git a/tests/main/whoami/successful_login.exp b/tests/main/whoami/successful_login.exp new file mode 100644 index 00000000..fcb4c8c7 --- /dev/null +++ b/tests/main/whoami/successful_login.exp @@ -0,0 +1,13 @@ +log_user 0 +spawn snap login $env(SPREAD_STORE_USER) + +expect "Password of " +send "$env(SPREAD_STORE_PASSWORD)\n" + +expect { + "Login successful" { + exit 0 + } default { + exit 1 + } +} diff --git a/tests/main/whoami/task.yaml b/tests/main/whoami/task.yaml new file mode 100644 index 00000000..2429b9d4 --- /dev/null +++ b/tests/main/whoami/task.yaml @@ -0,0 +1,18 @@ +summary: Checks for snap whoami + +# ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] + +restore: | + snap logout || true + +execute: | + echo "whoami before login" + snap whoami | MATCH "email: -" + + if [ -n "$SPREAD_STORE_USER" ] && [ -n "$SPREAD_STORE_PASSWORD" ]; then + expect -d -f successful_login.exp + + echo "whoami after login" + snap whoami | MATCH "email: $SPREAD_STORE_USER" + fi diff --git a/tests/main/writable-areas/task.yaml b/tests/main/writable-areas/task.yaml new file mode 100644 index 00000000..e592dac3 --- /dev/null +++ b/tests/main/writable-areas/task.yaml @@ -0,0 +1,37 @@ +summary: Check that snap apps and services can write to writable areas. + +environment: + # Ensure that running purely from the deb (without re-exec) works + # correctly + SNAP_REEXEC/reexec0: 0 + SNAP_REEXEC/reexec1: 1 + +prepare: | + snap pack $TESTSLIB/snaps/data-writer + +restore: | + rm -f data-writer_1.0_all.snap + +execute: | + snap install --dangerous data-writer_1.0_all.snap + + echo "Apps can write to writable areas" + data-writer.app + [ -f /var/snap/data-writer/x1/from-app ] + [ -f /var/snap/data-writer/common/from-app ] + [ -f /root/snap/data-writer/x1/from-app ] + # TODO: As soon as `snap run` is used (which creates this directory), + # uncomment the following line: + #[ -f /root/snap/data-writer/common/from-app ] + + echo "Waiting for data writer service to finish..." + while [ ! -f /root/snap/data-writer/x1/from-service ]; do + sleep 1 + done + + echo "Services can write to writable areas" + [ -f /var/snap/data-writer/x1/from-service ] + [ -f /var/snap/data-writer/common/from-service ] + # TODO: As soon as `snap run` is used (which creates this directory), + # uncomment the following line: + #[ -f /root/snap/data-writer/common/from-service ] diff --git a/tests/main/xauth-migration/task.yaml b/tests/main/xauth-migration/task.yaml new file mode 100644 index 00000000..7ed6c151 --- /dev/null +++ b/tests/main/xauth-migration/task.yaml @@ -0,0 +1,85 @@ +summary: Check file XAUTHORITY env variable points to is migrated into the snap environment +description: | + The XAUTHORITY environment variable points to a file which is located + in HOME, /run or /tmp depending on the distribution and desktop being + used. If the file ends up in /tmp then it wont be visible inside the + snap environment as each snap gets its private /tmp. To ensure the + file XAUTHORITY points to is always available no matter where it is + stored on the host system we migrate the file and copy it into + /run/$USER/.Xauthority and set the XAUTHORITY environment variable + accordingly. + + This test verified the correct behaviour of the implemented logic + inside `snap run`. + +execute: | + snap install test-snapd-tools + + ensure_xauth_path() { + export XAUTHORITY="$1" + # Get rid of things to ensure a clean test bed + rm -f /var/snap/test-snapd-tools/common/xauth-content /run/user/0/.Xauthority + snap run --shell test-snapd-tools.sh <<'EOF' + echo $XAUTHORITY > /var/snap/test-snapd-tools/common/xauth-content + exit + EOF + env_path="$(cat /var/snap/test-snapd-tools/common/xauth-content)" + test "$env_path" = "$2" || exit 1 + test "$(sh256sum $env_path)" = "$(sh256sum $1)" + unset XAUTHORITY + } + + mock_xauthority() { + # Generate valid Xauthority file which `snap run` will accept + rm -f $1; touch $1 + for ((c=0; c<=$2; c++)) + do + # Family + echo -n -e \\x01\\x00 >> $1 + # Address + echo -n -e \\x00\\x04\\x73\\x6e\\x61\\x70 >> $1 + # Number + echo -n -e \\x00\\x01\\xff >> $1 + # Name + echo -n -e \\x00\\x05\\x73\\x6e\\x61\\x70\\x64 >> $1 + # Data + echo -n -e \\x00\\x01\\xff >> $1 + done + } + + if [ ! -d /run/user/0 ]; then + mkdir -p /run/user/0 + chmod 700 /run/user/0 + fi + + # An invalid Xauthority file should cause the XAUTHORITY + # environment variable to stay untouched. + echo "foo bar" > /tmp/invalid-xauthority + ensure_xauth_path /tmp/invalid-xauthority /tmp/invalid-xauthority + test ! -e /run/user/0/.Xauthority + + echo > /tmp/invalid-xauthority + ensure_xauth_path /tmp/invalid-xauthority /tmp/invalid-xauthority + test ! -e /run/user/0/.Xauthority + + # Generate valid Xauthority file which `snap run` will accept + mock_xauthority /tmp/valid-xauthority 4 + chmod 600 /tmp/valid-xauthority + + # Xauthority should be correctly migrated + ensure_xauth_path /tmp/valid-xauthority /run/user/0/.Xauthority + test -e /run/user/0/.Xauthority + + # When we switch the owner the input xauth file shouldn't be moved. + chown 1000:1000 /tmp/valid-xauthority + ensure_xauth_path /tmp/valid-xauthority /tmp/valid-xauthority + test ! -e /run/user/0/.Xauthority + + # We should not be able to get things like /etc/shadow migrated + # into the snap environment. When `snap run` does the migration + # it will change the content of the XAUTHORITY env variable + # inside the snap environment and otherwise leave the variable + # untouched. This is why the expected content of the XAUTHORITY + # env variable in this case is /etc/shadow + ensure_xauth_path /etc/shadow /etc/shadow + test ! -e /run/user/0/.Xauthority diff --git a/tests/main/xdg-open-compat/task.yaml b/tests/main/xdg-open-compat/task.yaml new file mode 100644 index 00000000..d80c480a --- /dev/null +++ b/tests/main/xdg-open-compat/task.yaml @@ -0,0 +1,103 @@ +summary: Ensure the xdg-open still works in compatibility mode + +description: | + The core snap has a xdg-open binary that sends both to the + new io.snapcraft.Launcher and the old com.canonical.SafeLauncher + dbus session bus. This test ensures the compatibility with + the old launcher is still there for distros that do not re-exec + and still get a new core but still have the old snapd-xdg-open + package. + +# we must have snapd-xdg-open available +systems: [ubuntu-16.04-64] + +environment: + DISPLAY: :0 + XDG_OPEN_OUTPUT: /tmp/xdg-open-output + +restore: | + . "$TESTSLIB/dirs.sh" + . "$TESTSLIB/pkgdb.sh" + rm -f /usr/bin/xdg-open + rm -f $XDG_OPEN_OUTPUT + dpkg -r snapd-xdg-open + rm -f /usr/share/applications/defaults.list + rm -f /usr/share/applications/xdg-open.desktop + +execute: | + . "$TESTSLIB/pkgdb.sh" + . "$TESTSLIB/dirs.sh" + + # download from LP, it is not available in the archive anymore + wget https://launchpad.net/ubuntu/+source/snapd-xdg-open/0.0.0~16.04/+build/10503735/+files/snapd-xdg-open_0.0.0~16.04_amd64.deb + # snapd breaks older version of snapd-xdg-open so we cannot + # install them together. force things to work! + dpkg -i --force-all snapd-xdg-open_*.deb + + # setup some env so that g_app_info_launch_default_for_uri() works + cat > /usr/share/applications/defaults.list < /usr/share/applications/xdg-open.desktop < /usr/bin/xdg-open + #!/bin/sh + echo "$@" > /tmp/xdg-open-output + EOF + chmod +x /usr/bin/xdg-open + + ensure_xdg_open_output() { + rm -f $XDG_OPEN_OUTPUT + export DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS + $SNAP_MOUNT_DIR/core/current/usr/bin/xdg-open "$1" + # xdg-open is async so we need to give it a little bit of time + for i in $(seq 10); do + if [ -e $XDG_OPEN_OUTPUT ]; then + break + fi + sleep .5 + done + test -e $XDG_OPEN_OUTPUT + test "$(cat $XDG_OPEN_OUTPUT)" = "$1" + } + + # Ensure http, https and mailto work + ensure_xdg_open_output "https://snapcraft.io" + ensure_xdg_open_output "http://snapcraft.io" + ensure_xdg_open_output "mailto:talk@snapcraft.io" + + # Ensure other schemes are not passed through + rm $XDG_OPEN_OUTPUT + ! $SNAP_MOUNT_DIR/core/current/usr/bin/xdg-open ftp://snapcraft.io + test ! -e $XDG_OPEN_OUTPUT + ! $SNAP_MOUNT_DIR/core/current/usr/bin/xdg-open aabbcc + test ! -e $XDG_OPEN_OUTPUT diff --git a/tests/manual-tests.md b/tests/manual-tests.md new file mode 100644 index 00000000..0217c300 --- /dev/null +++ b/tests/manual-tests.md @@ -0,0 +1,236 @@ +# Test gadget snap with pre-installed snaps + +1. Branch snappy-systems +2. Modify the `snap.yaml` to add a snap, e.g.: + + ```diff + === modified file 'generic-amd64/meta/snap.yaml' + --- generic-amd64/meta/snap.yaml 2015-07-03 12:50:03 +0000 + +++ generic-amd64/meta/snap.yaml 2015-11-09 16:26:12 +0000 + @@ -7,6 +7,8 @@ + config: + ubuntu-core: + autopilot: true + + config-example-bash: + + msg: "huzzah\n" + + gadget: + branding: + @@ -20,3 +22,7 @@ + boot-assets: + files: + - path: grub.cfg + + + + software: + + built-in: + + - config-example-bash.canonical + ``` + + (for amd64, or modify for other arch). + +3. Build the gadget snap. +4. Create an image using the gadget snap. +5. Boot the image +6. Run: + + sudo journalctl -u snapd.firstboot.service + + * Check that it shows no errors. + + +7. Run: + + config-example-bash.hello + + * Check that it prints `huzzah`. + +# Test gadget snap with modules + +1. Branch snappy-systems +2. Modify the `snap.yaml` to add a module, e.g.: + + ```diff + === modified file 'generic-amd64/meta/snap.yaml' + --- generic-amd64/meta/snap.yaml 2015-07-03 12:50:03 +0000 + +++ generic-amd64/meta/snap.yaml 2015-11-12 10:14:30 +0000 + @@ -7,6 +7,7 @@ + config: + ubuntu-core: + autopilot: true + + load-kernel-modules: [tea] + + gadget: + branding: + + ``` + +3. Build the gadget snap. +4. Create an image using the gadget snap. +5. Boot the image. +6. Run: + + sudo journalctl -u snapd.firstboot.service + + * Check that it shows no errors. + + +7. Check that the output of `lsmod` includes the module you requested. With the above example, + + lsmod | grep tea + +# Test resize of writable partition + +1. Get the start of the *writable* partition: + + parted /path/to/ubuntu-snappy.img unit b print + + * Note down the number of bytes in the *Start* column for the *writable* partition. + +2. Make a loopback block device for the writable partition, replacing *{start}* with the number + from the previous step: + + sudo losetup -f --show -o {start} /path/to/ubuntu-snappy.img + + * Note down the loop device. + +3. Shrink the file system to the minimum, replacing *{dev}* with the device from the previous + step: + + sudo e2fsck -f {dev} + sudo resize2fs -M {dev} + +4. Delete the loopback block device: + + sudo losetup -d {dev} + +5. Get the end of the *writable* partition: + + parted /path/to/ubuntu-snappy.img unit b print + + * Note down the *Number* of the *writable* partition and the number of bytes in the *End* + column. + +6. Resize the *writable* partition, using the partition *{number}* from the last step, and + replacing the *{end}* with a value that leaves more than 10% space free at the end. + + parted /path/to/ubuntu-snappy.img unit b resizepart {number} {end*85%} + +7. Boot the image. + +8. Print the free space of the file system, replacing *{dev}* with the device that has the + *writable* partition: + + sudo parted -s {dev} unit % print free + + * Check that the writable partition was resized to occupy all the empty space. + +# Test Mir interface by running Mir kiosk snap examples + +1. Install Virtual Machine Manager +2. Stitch together a new image +3. Build both the mir-server and the mir-client snaps from lp:~mir-team/+junk/mir-server-snap and lp:~mir-team/+junk/snapcraft-mir-client +4. Copy over the snaps and sideload install the mir-server snap, which should result in a mir-server launching black blank screen with a mouse available. +5. Now install the mir-client snap. +6. Manually connect mir-client:mir to mir-server:mir due to bug 1577897, then start the mir-client service manually. +7. This should result in the Qt clock example app being displayed. + +# Test serial-port interface using miniterm app + +1. Using Ubuntu classic build and install a simple snap containing the Python + pySerial module. Define a app that runs the module and starts miniterm. + +```yaml + name: miniterm + version: 1 + summary: pySerial miniterm in a snap + description: | + Simple snap that contains the modules necessary to run + pySerial. Useful for testing serial ports. + confinement: strict + apps: + open: + command: python3 -m serial.tools.miniterm + plugs: [serial-port] + parts: + my-part: + plugin: nil + stage-packages: + - python3-serial +``` + +2. Ensure the 'serial-port' interface is connected to miniterm +3. Use sudo miniterm.open /dev/tty to open a serial port + +# Test pulseaudio interface using paplay, pactl + +1. Using a Snappy core image on a device like an RPi2/3, install the + build and install the simple-pulseaudio snap from the following + git repo: + git://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/examples +2. $ cd examples/simple-pulseaudio +3. Ensure that the 'pulseaudio' interface is connected to paplay + $ sudo snap interfaces +4. Use /snap/bin/simple-pulseaudio.pactl stat and verify that you see + valid output status from pulseaudio +5. Use /snap/bin/simple-pulseaudio.paplay $SNAP/usr/share/sounds/alsa/Noise.wav and verify + that you can hear the sound playing + +# Test bluetooth-control interface + +1. Using Ubuntu classic build and install the bluetooth-tests snap + from the store. + +2. Stop system BlueZ service + +$ sudo systemctl stop bluetooth + +or if you have the bluez snap installed + +$ snap remove bluez + +3. Run one of the tests provided by the bluetooth-tests snap + + $ sudo /snap/bin/bluetooth-tests.hci-tester + + and verify it actually passes. If some of the tests fail + there will be a problem with the particular kernel used on + the device. + +# Test tpm interface with tpm-tools + +1. Install tpm snap from store. +2. Connect plug tpm:tpm to slot ubuntu-core:tpm. +3. Reboot the system so daemon in tpm snap can get proper permissions. +4. Use tpm.version to read from tpm device and make sure it shows no error. + + $ tpm.version + xKV TPM 1.2 Version Info: + Chip Version: 1.2.5.81 + Spec Level: 2 + Errata Revision: 3 + TPM Vendor ID: WEC + Vendor Specific data: 0000 + TPM Version: 01010000 + Manufacturer Info: 57454300 + +# Test fwupd interface with uefi-fw-tools + +1. Ensure your BIOS support UEFI firmware upgrading via UEFI capsule format +2. Install the uefi-fw-tools snap from the store +3. Ensure the 'fwupd' interface is connected + + $ sudo snap connect uefi-fw-tools:fwupdmgr uefi-fw-tools:fwupd + +4. Check if the device support UEFI firmware updates + + $ sudo uefi-fw-tools.fwupdmgr get-devices + +5. Get available UEFI firmware from the server + + $ sudo uefi-fw-tools.fwupdmgr refresh + +6. Download firmware + + $ sudo uefi-fw-tools.fwupdmgr update + +7. Reboot and ensure it start the upgrading process diff --git a/tests/nested/core-revert/task.yaml b/tests/nested/core-revert/task.yaml new file mode 100644 index 00000000..7f662987 --- /dev/null +++ b/tests/nested/core-revert/task.yaml @@ -0,0 +1,73 @@ +summary: core revert test + +systems: [ubuntu-16.04-64] + +prepare: | + . "$TESTSLIB/nested.sh" + create_nested_core_vm + +restore: | + . "$TESTSLIB/nested.sh" + destroy_nested_core_vm + +debug: | + systemctl stop nested-vm || true + if [ -f /tmp/work-dir/ubuntu-core.img ]; then + loops=$(kpartx -avs /tmp/work-dir/ubuntu-core.img | cut -d' ' -f 3) + + part=$(echo "$loops" | tail -1) + + tmp=$(mktemp -d) + mount "/dev/mapper/$part" "$tmp" + + grep --text "hsearch_r failed for.* No such process" "$tmp/system-data/var/log/syslog" + + umount "$tmp" + rm -rf "$tmp" + kpartx -ds /tmp/work-dir/ubuntu-core.img + fi + +kill-timeout: 40m + +execute: | + . "$TESTSLIB/nested.sh" + + cd "$SPREAD_PATH" + execute_remote "sudo snap install network-manager" + execute_remote "sudo snap install bluez" + execute_remote "sudo bluez.bluetoothctl -a" + execute_remote "snap aliases" | MATCH "nmcli" + + # configure network-manager to take over the connection control on next reboot + execute_remote "sudo mv /etc/netplan/00-snapd-config.yaml /etc/netplan/00-snapd-config.yaml.back" + execute_remote "sudo sed -i 's/unmanaged-devices+=interface-name:eth*/#unmanaged-devices+=interface-name:eth*/' /var/snap/network-manager/current/conf.d/disable-ethernet.conf" + + execute_remote "snap info core" | MATCH "tracking: +${CORE_CHANNEL}" + execute_remote "sudo snap refresh --${CORE_REFRESH_CHANNEL} core" || true + + wait_for_ssh + + while ! execute_remote "snap changes" | MATCH "Done.*Refresh \"core\" snap from \"${CORE_REFRESH_CHANNEL}\" channel"; do sleep 1 ; done + execute_remote "snap info core" | MATCH "tracking: +${CORE_REFRESH_CHANNEL}" + # make sure network-manager is on charge of eth0 + execute_remote "nmcli c" | MATCH eth0 + execute_remote "nmcli d" | MATCH "eth0 +ethernet +connected" + execute_remote "sudo bluez.bluetoothctl -a" + # sanity check, no refresh should be done here but the command shouldn't fail + execute_remote "sudo snap refresh" + + execute_remote "snap aliases" | MATCH "nmcli" + + execute_remote "sudo snap revert core" || true + + wait_for_ssh + + while ! execute_remote "snap changes" | MATCH "Done.*Revert \"core\" snap"; do sleep 1 ; done + execute_remote "snap aliases" | MATCH "nmcli" + execute_remote "snap info core" | MATCH "tracking: +${CORE_REFRESH_CHANNEL}" + execute_remote "ifconfig" | MATCH eth0 + execute_remote "sudo bluez.bluetoothctl -a" + # this currently fails, refresh from the old version conflicts with aliases + # execute_remote "sudo snap refresh" + + execute_remote "sudo cat /var/log/syslog" | MATCH -v "hsearch_r failed for.* No such process" diff --git a/tests/nested/extra-snaps-assertions/task.yaml b/tests/nested/extra-snaps-assertions/task.yaml new file mode 100644 index 00000000..187cb8bc --- /dev/null +++ b/tests/nested/extra-snaps-assertions/task.yaml @@ -0,0 +1,67 @@ +summary: create ubuntu-core image and execute the suite in a nested qemu instance + +systems: [ubuntu-16.04-64, ubuntu-16.04-32] + +prepare: | + # FIXME: until https://github.com/snapcore/snapd/pull/3263 is available from + # the archive we need to build snapd from branch so that it can be used by + # ubuntu-image + + # first, remove snapd, in the prepare stage of the nested suite ubuntu-image is installed, + # before building snapd from the branch we check that the core is not present + apt remove -y --purge snapd + + . "$TESTSLIB/prepare.sh" + prepare_classic + prepare_each_classic + + snap install --classic --beta ubuntu-image + + # determine arch related vars + case "$NESTED_ARCH" in + amd64) + QEMU="$(which qemu-system-x86_64)" + ;; + i386) + QEMU="$(which qemu-system-i386)" + ;; + *) + echo "unsupported architecture" + exit 1 + ;; + esac + + # create ubuntu-core image + mkdir -p /tmp/work-dir + + snap download core + + /snap/bin/ubuntu-image --image-size 3G "$TESTSLIB/assertions/nested-${NESTED_ARCH}.model" --channel "$CORE_CHANNEL" --output ubuntu-core.img --extra-snaps core_*.snap + mv ubuntu-core.img /tmp/work-dir + + . "$TESTSLIB/nested.sh" + create_assertions_disk + + . "$TESTSLIB/systemd.sh" + systemd_create_and_start_unit nested-vm "${QEMU} -m 1024 -nographic -net nic,model=virtio -net user,hostfwd=tcp::8022-:22 -drive file=/tmp/work-dir/ubuntu-core.img,if=virtio,cache=none -drive file=${PWD}/assertions.disk,if=virtio,cache=none" + +restore: | + . "$TESTSLIB/systemd.sh" + systemd_stop_and_destroy_unit nested-vm + rm -rf /tmp/work-dir + +execute: | + . "$TESTSLIB/nested.sh" + wait_for_ssh + prepare_ssh + + cd "$SPREAD_PATH" + + echo "Wait for first boot to be done" + while ! execute_remote "snap changes" | MATCH "Done.*Initialize system state"; do sleep 1; done + + echo "We have a model assertion" + execute_remote "snap known model" | MATCH "series: 16" + + echo "Make sure core has an actual revision" + execute_remote "snap list" | MATCH "^core +[0-9]+\-[0-9.]+ +[0-9]+ +canonical +" diff --git a/tests/nested/image-build/task.yaml b/tests/nested/image-build/task.yaml new file mode 100644 index 00000000..67fe129a --- /dev/null +++ b/tests/nested/image-build/task.yaml @@ -0,0 +1,25 @@ +summary: create ubuntu-core image and execute the suite in a nested qemu instance + +systems: [ubuntu-16.04-64] + +prepare: | + . "$TESTSLIB/nested.sh" + create_nested_core_vm + +restore: | + . "$TESTSLIB/nested.sh" + destroy_nested_core_vm + +execute: | + cd "$SPREAD_PATH" + + tmp=$(mktemp -d) + trap 'rm -rf $tmp' EXIT + curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz && rm -f spread-amd64.tar.gz && mv spread "$tmp" + + set +x + export SPREAD_EXTERNAL_ADDRESS=localhost:8022 + "$tmp/spread" -v external:ubuntu-core-16-64:tests/main/ubuntu-core-reboot \ + external:ubuntu-core-16-64:tests/main/install-store \ + external:ubuntu-core-16-64:tests/main/interfaces-system-observe \ + external:ubuntu-core-16-64:tests/main/op-remove-retry | while read -r line; do echo "> $line"; done diff --git a/tests/nightly/docker/task.yaml b/tests/nightly/docker/task.yaml new file mode 100644 index 00000000..e7df1a7d --- /dev/null +++ b/tests/nightly/docker/task.yaml @@ -0,0 +1,37 @@ +summary: Check that the docker snap works +systems: [ubuntu-16.04-*, ubuntu-core-16-*, ubuntu-14.04-64] + +prepare: | + modprobe aufs + # we don't distort the download statistics, we run with SNAPPY_TESTING=1 + # which will add "testing" to the user-agent header and the store does + # not count this. + snap install docker + +debug: | + cat /var/log/syslog + dmesg + +execute: | + # wait for the socket to be listening + while ! printf "GET /" | nc -U -q 1 /var/run/docker.sock; do sleep 1; done + + echo "Check that docker info and run basically work" + docker info + + prefix="" + case "$SPREAD_SYSTEM" in + "ubuntu-core-16-arm-32") + prefix=armhf/ + ;; + "ubuntu-core-16-arm-64") + prefix=aarch64/ + ;; + "ubuntu-core-16-32") + prefix=i386/ + ;; + "ubuntu-16.04-32") + prefix=i386/ + ;; + esac + docker run --rm ${prefix}hello-world | MATCH "Hello from Docker" diff --git a/tests/nightly/unity/task.yaml b/tests/nightly/unity/task.yaml new file mode 100644 index 00000000..e575b19e --- /dev/null +++ b/tests/nightly/unity/task.yaml @@ -0,0 +1,31 @@ +summary: Check that a unity snap can start and its window is shown + +environment: + DISPLAY: ":99.0" + +systems: [ubuntu-16.04-64] + +prepare: | + # the file /etc/init/tty1.conf is present in the default images, upstart + # (which is installed as a dependency of the required packages) ships it + # and doesn't install cleanly if that file is in place + mv /etc/init/tty1.conf /etc/init/tty1.conf.back + + apt install -y --no-install-recommends unity + +disabled_restore: | + systemctl stop unity-app + apt autoremove -y --purge unity + + mv /etc/init/tty1.conf.back /etc/init/tty1.conf + +execute: | + echo "Given a unity snap is installed" + snap install ubuntu-clock-app + + echo "When the app is started" + systemd-run --unit unity-app --setenv=DISPLAY="$DISPLAY" --uid "$(id -u test)" "$(which xvfb-run)" --server-args="$DISPLAY -screen 0 1200x960x24 -ac +extension RANDR" "$(which ubuntu-clock-app.clock)" + + echo "Then the app window is created" + expected=".*?\"qmlscene: clockMainView\": \(\"qmlscene\" \"com\.ubuntu\.clock\"\)" + while ! xwininfo -tree -root | grep -Pq "$expected"; do sleep 1; done diff --git a/tests/regression/lp-1595444/task.yaml b/tests/regression/lp-1595444/task.yaml new file mode 100644 index 00000000..c56ca2fd --- /dev/null +++ b/tests/regression/lp-1595444/task.yaml @@ -0,0 +1,35 @@ +summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1595444 + +details: | + This task checks the behavior of snap-confine when it is started from + a directory that doesn't exist in the execution environment (chroot). + +systems: + # This test only applies to classic systems + - -ubuntu-core-16-* + # No confinement (AppArmor, Seccomp) available on these systems + - -debian-* + - -fedora-* + - -opensuse-* + +prepare: | + echo "Having installed the test snap" + . "$TESTSLIB/snaps.sh" + install_local test-snapd-tools + echo "Hanving created a directory not present in the core snap" + mkdir -p "/foo" + +execute: | + echo "We can go to a location that is available in all snaps (/tmp)" + echo "We can run the 'pwd' tool and it reports /tmp" + [ "$(cd /tmp && test-snapd-tools.cmd pwd)" = "/tmp" ] + echo "But if we go to a location that is not available to snaps (e.g. /foo)" + echo "Then snap-confine moves us to /var/lib/snapd/void" + [ "$(cd /foo && test-snapd-tools.cmd pwd)" = "/var/lib/snapd/void" ] + echo "And that directory is not readable or writable" + [ "$(cd /foo && test-snapd-tools.cmd ls 2>&1)" = "ls: cannot open directory '.': Permission denied" ]; + +restore: | + rm -f -d /foo + # NOTE: the snap is blocked by apparmor from reading /var/lib/snapd/void + dmesg -c diff --git a/tests/regression/lp-1597839/task.yaml b/tests/regression/lp-1597839/task.yaml new file mode 100644 index 00000000..76e30d5b --- /dev/null +++ b/tests/regression/lp-1597839/task.yaml @@ -0,0 +1,13 @@ +summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1597839 +# This test only applies to classic systems +systems: [-ubuntu-core-16-*, -fedora-*] +details: | + The snappy execution environment should contain the /lib/modules directory + from the host filesystem when running on a classic distribution +prepare: | + echo "Having installed the test snap" + . "$TESTSLIB/snaps.sh" + install_local test-snapd-tools +execute: | + echo "We can ensure that the /lib/modules/$(uname -r) directory exists" + test-snapd-tools.cmd test -d "/lib/modules/$(uname -r)" diff --git a/tests/regression/lp-1597842/task.yaml b/tests/regression/lp-1597842/task.yaml new file mode 100644 index 00000000..6755f9f0 --- /dev/null +++ b/tests/regression/lp-1597842/task.yaml @@ -0,0 +1,23 @@ +summary: Regression test for https://bugs.launchpad.net/snap-confine/+bug/1597842 +details: | + The snap execution environment is expected to contain the /usr/src + directory from the classic distribution. Certain snaps may use that to + access kernel sources. +# This test only applies to classic systems +systems: [-ubuntu-core-16-*, -fedora-*] +prepare: | + echo "Having installed the test snap" + . "$TESTSLIB/snaps.sh" + # NOTE: devmode is required because there's no interface for reading /usr/src/.canary + install_local_devmode test-snapd-tools + echo "Having prepared a canary file in /usr/src/.canary" + mv /usr/src /usr/src.orig || true + mkdir -p /usr/src + echo canary > /usr/src/.canary +execute: | + echo "The canary file in /usr/src can be read" + [ "$(test-snapd-tools.cmd cat /usr/src/.canary)" = "canary" ] +restore: | + rm -f /usr/src/.canary + rm -f -d /usr/src + mv /usr/src.orig /usr/src || true diff --git a/tests/regression/lp-1599891/task.yaml b/tests/regression/lp-1599891/task.yaml new file mode 100644 index 00000000..cbdf1435 --- /dev/null +++ b/tests/regression/lp-1599891/task.yaml @@ -0,0 +1,12 @@ +summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1599891 +systems: + # No confinement (AppArmor, Seccomp) available on these systems + - -debian-* + - -fedora-* + - -opensuse-* +execute: | + snap_confine=/usr/lib/snapd/snap-confine + echo "Seeing that snap-confine is in $snap_confine" + + echo "I also see a corresponding apparmor profile" + MATCH "$snap_confine \(enforce\)" < /sys/kernel/security/apparmor/profiles diff --git a/tests/regression/lp-1606277/task.yaml b/tests/regression/lp-1606277/task.yaml new file mode 100644 index 00000000..40c52c47 --- /dev/null +++ b/tests/regression/lp-1606277/task.yaml @@ -0,0 +1,13 @@ +summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1606277 +details: | + A missing bind mount for /var/log prevents access to system log files + even if the log-observe interface is being used. +prepare: | + . "$TESTSLIB/snaps.sh" + echo "Having installed a test snap" + install_local log-observe-consumer + echo "And having connected the log-observe interface" + snap connect log-observe-consumer:log-observe :log-observe +execute: | + echo "We can now see a non-empty /var/log directory" + [ "$(log-observe-consumer.cmd ls /var/log | wc -l)" != 0 ] diff --git a/tests/regression/lp-1607796/task.yaml b/tests/regression/lp-1607796/task.yaml new file mode 100644 index 00000000..bac4198a --- /dev/null +++ b/tests/regression/lp-1607796/task.yaml @@ -0,0 +1,12 @@ +summary: Check that /root is bind mounted to the real /root +prepare: | + echo "Having installed a test snap in devmode" + . "$TESTSLIB/snaps.sh" + install_local_devmode test-snapd-tools + echo "Having added a canary file in /root" + echo "test" > /root/canary +execute: | + echo "We can see the canary file in /root" + [ "$(test-snapd-tools.cmd cat /root/canary)" = "test" ] +restore: | + rm -f /root/canary diff --git a/tests/regression/lp-1613845/task.yaml b/tests/regression/lp-1613845/task.yaml new file mode 100644 index 00000000..7c28f5b9 --- /dev/null +++ b/tests/regression/lp-1613845/task.yaml @@ -0,0 +1,22 @@ +summary: Check that /var/lib/lxd is bind mounted to the real thing if one exists +details: | + After switching to the chroot-based snap-confine the LXD snap stopped + working (even in devmode) because it relied on access to /var/lib/lxd from + the host filesystem. While this would never work in an all-snap image it is + still important to ensure that it works in classic devmode environment. +# This test only applies to classic systems +systems: [-ubuntu-core-16-*, -fedora-*] +prepare: | + echo "Having installed the test snap in devmode" + . "$TESTSLIB/snaps.sh" + install_local_devmode test-snapd-tools + echo "Having created a canary file in /var/lib/lxd" + mkdir -p /var/lib/lxd + echo "test" > /var/lib/lxd/canary +execute: | + echo "We can see the canary file in /var/lib/lxd" + [ "$(test-snapd-tools.cmd cat /var/lib/lxd/canary)" = "test" ] +restore: | + rm -f /var/lib/lxd/canary + # When the host already has LXD installed we cannot just remove this + rmdir /var/lib/lxd || : diff --git a/tests/regression/lp-1615113/task.yaml b/tests/regression/lp-1615113/task.yaml new file mode 100644 index 00000000..08a4584d --- /dev/null +++ b/tests/regression/lp-1615113/task.yaml @@ -0,0 +1,13 @@ +summary: Check that entire snap can be shared with the content interface +prepare: | + echo "Having installed a pair of snaps that share content" + . "$TESTSLIB/snaps.sh" + install_local test-snapd-content-slot + install_local test-snapd-content-plug + echo "We can connect them together" + snap connect test-snapd-content-plug:shared-content-plug test-snapd-content-slot:shared-content-slot +execute: | + echo "We can now see that the content is shared" + test-snapd-content-plug.content-plug | grep "Some shared content" + echo "And fstab files are created" + [ "$(find /var/lib/snapd/mount -type f -name '*.fstab' | wc -l)" -gt 0 ] diff --git a/tests/regression/lp-1618683/task.yaml b/tests/regression/lp-1618683/task.yaml new file mode 100644 index 00000000..6ee04955 --- /dev/null +++ b/tests/regression/lp-1618683/task.yaml @@ -0,0 +1,13 @@ +summary: Check that user namespace can be unshared within snap apps +details: | + Snap-confine used to "leak" the root filesystem directory across the + pivot_root call. This caused checks in the kernel to fail and resulted in + the inability to create user namespaces from sufficiently privileged or + devmode snaps. +prepare: | + echo "Having installed a test snap in devmode" + . "$TESTSLIB/snaps.sh" + install_local_devmode test-snapd-tools +execute: | + echo "We can run unshare -U as a regular user and expect it to work" + test-snapd-tools.cmd unshare -U true diff --git a/tests/regression/lp-1630479/task.yaml b/tests/regression/lp-1630479/task.yaml new file mode 100644 index 00000000..70b62efe --- /dev/null +++ b/tests/regression/lp-1630479/task.yaml @@ -0,0 +1,27 @@ +summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1630479 +details: | + The PATH environment variable needs to make sense for the layout of the + core snap. Snap-confine contains code that sets it accordingly but during + the introduction of the namespace sharing feature that code would run only + when the namespace was initially constructed. Subsequent calls would not + see the correct path. +prepare: | + echo "Having installed a test snap" + . "$TESTSLIB/snaps.sh" + install_local_devmode test-snapd-tools +execute: | + . "$TESTSLIB/dirs.sh" + echo "We can observe the value of PATH twice" + revision=$(readlink "$SNAP_MOUNT_DIR/test-snapd-tools/current") + one="$(test-snapd-tools.cmd sh -c 'echo $PATH')" + two="$(test-snapd-tools.cmd sh -c 'echo $PATH')" + echo "We can see that PATH is stable across calls" + [ "$one" = "$two" ] + echo "We can make sure that it has the right value" + [ "$one" = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games" ] + # NOTE: the following parts are notably absent from PATH + # - $SNAP_MOUNT_DIR/test-snapd-tools/$revision/bin + # - $SNAP_MOUNT_DIR/test-snapd-tools/$revision/usr/bin + # + # This is because our test snap is not built with snapcraft and those path + # elements are only added by the wrappers generated by snapcraft. diff --git a/tests/regression/lp-1641885/task.yaml b/tests/regression/lp-1641885/task.yaml new file mode 100644 index 00000000..939d626c --- /dev/null +++ b/tests/regression/lp-1641885/task.yaml @@ -0,0 +1,30 @@ +summary: snaps installed with --jailmode are not in devmode +systems: + # No confinement (AppArmor, Seccomp) available on these systems + - -debian-* + - -fedora-* + - -opensuse-* +details: | + Users found that a snap that uses "confinement: devmode", even when + installed with "snap install --jailmode" is effectively in devmode (the + apparmor profile is in complain mode) even if "snap list" reports it as + "jailmode". + This has been reported as https://bugs.launchpad.net/snappy/+bug/1641885 +prepare: | + snap pack "$TESTSLIB/snaps/test-snapd-devmode" + echo "Install a test snap that uses 'confinement: devmode' in jailmode" + snap install --jailmode --dangerous ./test-snapd-devmode_1.0_all.snap +execute: | + echo "Ensure that the snap is installed in jailmode" + snap list | MATCH 'test-snapd-devmode.*jailmode' + echo "Ensure that the apparmor profile doesn't use the complain mode" + grep attach_disconnected /var/lib/snapd/apparmor/profiles/snap.test-snapd-devmode.test-snapd-devmode | MATCH -v complain + echo "Ensure that the seccomp profile doesn't use the complain mode" + MATCH -v '@complain' < /var/lib/snapd/seccomp/bpf/snap.test-snapd-devmode.test-snapd-devmode.src +restore: | + rm -f ./test-snapd-devmode_1.0_all.snap +debug: | + echo "Apparmor profile (first 30 lines)" + head -n 30 /var/lib/snapd/apparmor/profiles/snap.test-snapd-devmode.test-snapd-devmode || true + echo "Seccomp profile (first 30 lines)" + head -n 30 /var/lib/snapd/seccomp/bpf/snap.test-snapd-devmode.test-snapd-devmode.src || true diff --git a/tests/regression/lp-1644439/task.yaml b/tests/regression/lp-1644439/task.yaml new file mode 100644 index 00000000..ffee2e30 --- /dev/null +++ b/tests/regression/lp-1644439/task.yaml @@ -0,0 +1,48 @@ +summary: Regression test for https://bugs.launchpad.net/snap-confine/+bug/1644439 +manual: true # see https://github.com/snapcore/snapd/pull/3076 +details: | + snap-confine uses privately-shared /run/snapd/ns to store bind-mounted + mount namespaces of each snap. In the case that snap-confine is invoked + from the mount namespace it typically constructs, the said directory does + not contain mount entries for preserved namespaces as those are only + visible in the main, outer namespace. In order to operate in such an + environment snap-confine must first re-associate its own process with + another namespace in which the /run/snapd/ns directory is visible. + The most obvious candidate is pid one, which definitely doesn't run in a + snap-specific namespace, has a predictable PID and is long lived. +prepare: | + echo "Having installed the test snap in devmode" + . "$TESTSLIB/snaps.sh" + install_local_devmode test-snapd-tools +execute: | + # This test is meaningful only on Ubuntu for now as this is where we have + # the complete apparmor patch-set. + . /etc/os-release + if [ "$ID" != "ubuntu" ] && [ "$ID" != "ubuntu-core" ]; then + echo "This test is only supported on Ubuntu" + exit 0 + fi + # Don't test on other architectures as (especially on arm) kernel versions + # are not synchronized with x86 and this test is not architecture specific + # to warrant the extra work to figure out which kernel revision got the fix + # to apparmor that this test depends on. + if [ "$(uname -m)" != x86_64 ] && [ "$(uname -m)" != i686 ]; then + echo "This test is only supported on x86_64 and i386" + exit 0 + fi + # Check if the kernel is at least 4.4.0-67 + if ! uname -r | perl -ne '/^(\d+)\.(\d+)\.(\d+)-(\d+)/ or exit 1; exit 1 if $1<4; exit 1 if $2<4; exit 1 if $3==0 && $4<67'; then + echo "This test is not supported on kernels older than 4.4.0-67" + exit 0 + fi + . "$TESTSLIB/dirs.sh" + echo "We can now run a snap command from the namespace of a snap command and see it work" + test-snapd-tools.cmd /bin/true + test-snapd-tools.cmd /bin/sh -c "SNAP_CONFINE_DEBUG=yes $SNAP_MOUNT_DIR/bin/test-snapd-tools.cmd /bin/true" + echo "We can now discard the namespace and repeat the test as a non-root user" + /usr/lib/snapd/snap-discard-ns test-snapd-tools + su -l -c 'test-snapd-tools.cmd /bin/true' test + su -l -c "test-snapd-tools.cmd /bin/sh -c \"SNAP_CONFINE_DEBUG=yes $SNAP_MOUNT_DIR/bin/test-snapd-tools.cmd /bin/true\"" test +debug: | + # Kernel version is an important input in understing failures of this test + uname -a diff --git a/tests/regression/lp-1665004/task.yaml b/tests/regression/lp-1665004/task.yaml new file mode 100644 index 00000000..88614fc9 --- /dev/null +++ b/tests/regression/lp-1665004/task.yaml @@ -0,0 +1,15 @@ +summary: ensure that /var/lib/snapd/hostfs is group-owned by root +details: | + On a system that never ran any snap before the /var/lib/snapd/hostfs + directory does not exist. When snap-confine is used it will create the + directory on demand but that directory will retain the group identity of + the user. +prepare: | + . "$TESTSLIB/snaps.sh" + install_local test-snapd-tools + if [ -d /var/lib/snapd/hostfs ]; then + rmdir /var/lib/snapd/hostfs; + fi +execute: | + test-snapd-tools.cmd true + [ "$(stat -c '%g' /var/lib/snapd/hostfs)" -eq 0 ] diff --git a/tests/regression/lp-1667385/task.yaml b/tests/regression/lp-1667385/task.yaml new file mode 100644 index 00000000..05a00871 --- /dev/null +++ b/tests/regression/lp-1667385/task.yaml @@ -0,0 +1,17 @@ +summary: Regression check for https://bugs.launchpad.net/snappy/+bug/1667385 +systems: [ubuntu-*] +details: | + When disabling and then enabling a snap, the flags saved in state + (e.g. from when the user installed it) should be preserved. +environment: + FLAG/jailmode: jailmode + FLAG/devmode: devmode + SNAP: test-snapd-devmode +prepare: | + snap install --edge "--$FLAG" "$SNAP" +execute: | + # sanity check + snap list $SNAP | MATCH "$FLAG$" + snap disable "$SNAP" + snap enable "$SNAP" + snap list "$SNAP" | MATCH "$FLAG$" diff --git a/tests/regression/lp-1693042/task.yaml b/tests/regression/lp-1693042/task.yaml new file mode 100644 index 00000000..71ece0f1 --- /dev/null +++ b/tests/regression/lp-1693042/task.yaml @@ -0,0 +1,14 @@ +summary: Regression check for https://bugs.launchpad.net/snapd/+bug/1693042 +details: | + When attempting to refresh to a bogus channel, detect it and fail. +execute: | + # sanity check + snap list core + + out=$(! snap refresh core --channel bogus 2>&1 1>- ) + revno=$( snap info core | awk '/^installed:/{print $3}' ) + if [[ "$revno" =~ x[0-9]+ ]]; then + MATCH 'error: snap "core" is local' <<< "$out" + else + MATCH not.found <<< "$out" + fi diff --git a/tests/regression/lp-1704860/snap-env-query.sh b/tests/regression/lp-1704860/snap-env-query.sh new file mode 100755 index 00000000..0a764a4d --- /dev/null +++ b/tests/regression/lp-1704860/snap-env-query.sh @@ -0,0 +1 @@ +env diff --git a/tests/regression/lp-1704860/task.yaml b/tests/regression/lp-1704860/task.yaml new file mode 100644 index 00000000..051606ae --- /dev/null +++ b/tests/regression/lp-1704860/task.yaml @@ -0,0 +1,25 @@ +summary: Work-in-progress on reproducing lp:1704860 +systems: [ubuntu-16.04-64] +details: | + In this bug, an app belonging go a snap using classic confinement confuses + the re-execution system in a way that causes distribution version of + snap-confine to be used, instead of the one from the core snap. If the + version outside and inside are different and incompatible the classily + confined snap will malfunction. + + This specifically happens when the distribution uses snapd 2.25 and the + core snap has snapd 2.26.9 + + Testing is somewhat complex but we can approximate by observing the value + of SNAP_DID_REEXEC as set inside the environment set up by snap run + --shell. Since neither snap-confine nor snap-exec re-execute themselves + (instead they rely on snap run to run the right tool in the first place) + this is safe to do. +execute: | + . $TESTSLIB/snaps.sh + install_local_classic test-snapd-classic-confinement + # We don't want to see SNAP_DID_REEXEC being set. + if snap run --shell test-snapd-classic-confinement ./snap-env-query.sh | grep 'SNAP_DID_REEXEC='; then + echo "SNAP_DID_REEXEC environment is set - it should *not* be set ever" + exit 1 + fi diff --git a/tests/regression/lp-1732555/task.yaml b/tests/regression/lp-1732555/task.yaml new file mode 100644 index 00000000..74ca88d3 --- /dev/null +++ b/tests/regression/lp-1732555/task.yaml @@ -0,0 +1,15 @@ +summary: installing a snap with unknown plugs and slots is harmless +details: > + Users have painfully found that a version of snapd crashed when a snap + contained unknown interfaces in either plugs or slots. +prepare: | + . "$TESTSLIB/snaps.sh" + install_local_devmode test-snapd-unknown-interfaces +execute: | + echo "Snapd did not die on us" + snap version + echo "The snap was installed and can be used" + test-snapd-unknown-interfaces -c true + echo "The bogus plugs and slots are not added" + snap interfaces | MATCH -v bogus-plug + snap interfaces | MATCH -v bogus-slot diff --git a/tests/unit/c-unit-tests-clang/task.yaml b/tests/unit/c-unit-tests-clang/task.yaml new file mode 100644 index 00000000..b86fb5b8 --- /dev/null +++ b/tests/unit/c-unit-tests-clang/task.yaml @@ -0,0 +1,30 @@ +summary: Build the test suite for C code using clang and run it +prepare: | + # Sanity check, the core snap is installed + snap info core | MATCH "installed:" + # Install build dependencies for the test + dpkg --get-selections > pkg-list + # Remove any autogarbage from sent by developer + rm -rf "$SPREAD_PATH/cmd/"{autom4te.cache,configure,test-driver,config.status,config.guess,config.sub,config.h.in,compile,install-sh,depcomp,build,missing,aclocal.m4,Makefile,Makefile.in} + make -C "$SPREAD_PATH/cmd" distclean || true + # install clang + . "$TESTSLIB/pkgdb.sh" + distro_install_package clang +execute: | + cd "$SPREAD_PATH/cmd/" + build_dir="$SPREAD_PATH/cmd/autogarbage" + BUILD_DIR=$build_dir CC=clang ./autogen.sh + cd $build_dir + # Build and run unit tests + make check +restore: | + # Remove autogarbage leftover from testing + rm -rf "$SPREAD_PATH/cmd/"{autom4te.cache,configure,test-driver,config.status,config.guess,config.sub,config.h.in,compile,install-sh,depcomp,build,missing,aclocal.m4,Makefile,Makefile.in} + # Remove the build tree + rm -rf "$SPREAD_PATH/cmd/autogarbage/" + # Remove any installed packages + dpkg --set-selections < pkg-list + rm -f pkg-list +debug: | + # Show the test suite failure log if there's one + cat "$SPREAD_PATH/cmd/autogarbage/test-suite.log" || true diff --git a/tests/unit/c-unit-tests-gcc/task.yaml b/tests/unit/c-unit-tests-gcc/task.yaml new file mode 100644 index 00000000..8d525c87 --- /dev/null +++ b/tests/unit/c-unit-tests-gcc/task.yaml @@ -0,0 +1,27 @@ +summary: Build the test suite for C code using gcc and run it +prepare: | + # Sanity check, the core snap is installed + snap info core | MATCH "installed:" + # Install build dependencies for the test + dpkg --get-selections > pkg-list + # Remove any autogarbage from sent by developer + rm -rf "$SPREAD_PATH/cmd/"{autom4te.cache,configure,test-driver,config.status,config.guess,config.sub,config.h.in,compile,install-sh,depcomp,build,missing,aclocal.m4,Makefile,Makefile.in} + make -C "$SPREAD_PATH/cmd" distclean || true +execute: | + cd "$SPREAD_PATH/cmd/" + build_dir="$SPREAD_PATH/cmd/autogarbage" + BUILD_DIR=$build_dir ./autogen.sh + cd $build_dir + # Build and run unit tests + make check +restore: | + # Remove autogarbage leftover from testing + rm -rf "$SPREAD_PATH/cmd/"{autom4te.cache,configure,test-driver,config.status,config.guess,config.sub,config.h.in,compile,install-sh,depcomp,build,missing,aclocal.m4,Makefile,Makefile.in} + # Remove the build tree + rm -rf "$SPREAD_PATH/cmd/autogarbage/" + # Remove any installed packages + dpkg --set-selections < pkg-list + rm -f pkg-list +debug: | + # Show the test suite failure log if there's one + cat "$SPREAD_PATH/cmd/autogarbage/test-suite.log" || true diff --git a/tests/unit/gccgo/task.yaml b/tests/unit/gccgo/task.yaml new file mode 100644 index 00000000..ae814afc --- /dev/null +++ b/tests/unit/gccgo/task.yaml @@ -0,0 +1,17 @@ +summary: Check that snapd builds with gccgo +# We only need to check that snapd builds with gccgo in one architecture. +systems: [ubuntu-16.04-64] +prepare: | + echo Installing gccgo-6 and pretending it is the default go + ln -s /usr/bin/go-6 /usr/local/bin/go +restore: | + rm -f /usr/local/bin/go +execute: | + echo Ensure we really build with gccgo + go version|MATCH gccgo + echo Build the deb with gccgo and run the tests as part of the build + su - -c "cd $GOHOME/src/github.com/snapcore/snapd && dpkg-buildpackage -tc -Zgzip" test + +# Tests run during package build take a while. +warn-timeout: 8m +kill-timeout: 20m diff --git a/tests/unit/go/task.yaml b/tests/unit/go/task.yaml new file mode 100644 index 00000000..88ee7c91 --- /dev/null +++ b/tests/unit/go/task.yaml @@ -0,0 +1,30 @@ +summary: Run project static and unit tests + +restore: | + rm -rf /tmp/static-unit-tests + +execute: | + mkdir -p /tmp/static-unit-tests/src/github.com/snapcore + cp -ar "$PROJECT_PATH" /tmp/static-unit-tests/src/github.com/snapcore + chown -R test:12345 /tmp/static-unit-tests + + # remove leftovers + rm -r /tmp/static-unit-tests/src/github.com/snapcore/snapd/vendor/*/ + rm -rf /tmp/static-unit-tests/src/github.com/snapcore/snapd/cmd/{autom4te.cache,configure,test-driver,config.status,config.guess,config.sub,config.h.in,compile,install-sh,depcomp,build,missing,aclocal.m4,Makefile,Makefile.in} + + su -l -c "cd /tmp/static-unit-tests/src/github.com/snapcore/snapd && PATH=$PATH GOPATH=/tmp/static-unit-tests ./run-checks --static" test + su -l -c "cd /tmp/static-unit-tests/src/github.com/snapcore/snapd && \ + TRAVIS_BUILD_NUMBER=$TRAVIS_BUILD_NUMBER \ + TRAVIS_BRANCH=$TRAVIS_BRANCH \ + TRAVIS_COMMIT=$TRAVIS_COMMIT \ + TRAVIS_JOB_NUMBER=$TRAVIS_JOB_NUMBER \ + TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST \ + TRAVIS_JOB_ID=$TRAVIS_JOB_ID \ + TRAVIS_REPO_SLUG=$TRAVIS_REPO_SLUG \ + TRAVIS_TAG=$TRAVIS_TAG \ + PATH=$PATH \ + COVERMODE=$COVERMODE \ + TRAVIS=true \ + CI=true \ + GOPATH=/tmp/static-unit-tests \ + ./run-checks --unit" test diff --git a/tests/upgrade/basic/task.yaml b/tests/upgrade/basic/task.yaml new file mode 100644 index 00000000..b4903ce9 --- /dev/null +++ b/tests/upgrade/basic/task.yaml @@ -0,0 +1,88 @@ +summary: Check that upgrade works + +systems: [-debian-sid-*] + +restore: | + if [ "$REMOTE_STORE" = staging ]; then + echo "skip upgrade tests while talking to the staging store" + exit 0 + fi + rm -f /var/tmp/myevil.txt + +execute: | + if [ "$REMOTE_STORE" = staging ]; then + echo "skip upgrade tests while talking to the staging store" + exit 0 + fi + . "$TESTSLIB/pkgdb.sh" + . "$TESTSLIB/snaps.sh" + + echo "Remove snapd and snap-confine" + distro_purge_package snapd snapd-selinux snap-confine || true + + echo "Install previous snapd version from the store" + distro_install_package snap-confine snapd + + prevsnapdver=$(snap --version|grep "snapd ") + + if [[ "$SPREAD_SYSTEM" = debian-* ]] ; then + # For debian we install the latest core snap independently until + # the bug fix is on stable once 2.27 landed + snap install core + fi + + echo "Install sanity check snaps with it" + snap install test-snapd-tools + snap install test-snapd-auto-aliases + # transitional: drop the "snap pack" check once it's released + do_classic=no + if is_classic_confinement_supported && snap pack --help >&/dev/null ; then + install_local_classic test-snapd-classic-confinement + do_classic=yes + fi + + echo "Sanity check installs" + test-snapd-tools.echo Hello | grep Hello + test-snapd-tools.env | grep SNAP_NAME=test-snapd-tools + test_snapd_wellknown1|MATCH "ok wellknown 1" + test_snapd_wellknown2|MATCH "ok wellknown 2" + + echo "Do upgrade" + # allow-downgrades prevents errors when new versions hit the archive, for instance, + # trying to install 2.11ubuntu1 over 2.11+0.16.04 + pkg_extension="$(distro_get_package_extension)" + distro_install_local_package --allow-downgrades "$GOHOME"/snap*."$pkg_extension" + + snapdver=$(snap --version|grep "snapd ") + [ "$snapdver" != "$prevsnapdver" ] + + echo "Sanity check already installed snaps after upgrade" + snap list | grep core + snap list | grep test-snapd-tools + test-snapd-tools.echo Hello | grep Hello + test-snapd-tools.env | grep SNAP_NAME=test-snapd-tools + if [ "$do_classic" = yes ]; then + test-snapd-classic-confinement.recurse 5 + fi + + # only test if confinement works and we actually have apparmor available + # FIXME: this will be converted to a better check once we added the + # plumbing for that into the snap command. + if [ -e /sys/kernel/security/apparmor ]; then + echo Hello > /var/tmp/myevil.txt + if test-snapd-tools.cat /var/tmp/myevil.txt; then + exit 1 + fi + fi + + # check that automatic aliases survived + test_snapd_wellknown1|MATCH "ok wellknown 1" + test_snapd_wellknown2|MATCH "ok wellknown 2" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown1 +test_snapd_wellknown1 +-" + snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2 +-" + + echo "Check migrating to types in state" + coreType=$(jq -r '.data.snaps["core"].type' /var/lib/snapd/state.json) + testSnapType=$(jq -r '.data.snaps["test-snapd-tools"].type' /var/lib/snapd/state.json) + [ "$coreType" = "os" ] + [ "$testSnapType" = "app" ] diff --git a/tests/upgrade/snapd-xdg-open/task.yaml b/tests/upgrade/snapd-xdg-open/task.yaml new file mode 100644 index 00000000..9d093f4d --- /dev/null +++ b/tests/upgrade/snapd-xdg-open/task.yaml @@ -0,0 +1,38 @@ +summary: Verify snapd-xdg-open package is properly replaced with the snapd one +description: | + snapd-xdg-open was formerly provided by the snapd-xdg-open package + and is now part of the snapd package. This test case verifies that + the snapd-xdg-open package from the archive is properly replaced. +# Test case only applies to Ubuntu as that is the only distribution where +# we had a snapd-xdg-open package ever. +systems: [-ubuntu-core-*, -debian-*, -ubuntu-14.04-*, -fedora-*] +restore: | + . "$TESTSLIB/pkgdb.sh" + if [ "$REMOTE_STORE" = staging ]; then + echo "skip upgrade tests while talking to the staging store" + exit 0 + fi + distro_purge_package snapd-xdg-open || true +execute: | + . "$TESTSLIB/pkgdb.sh" + + # Original version of snapd-xdg-open in 16.04 which was not + # part of the snapd source package. + ver=0.0.0~16.04 + if ! distro_install_package snapd-xdg-open=$ver; then + # version of snapd-xdg-open in 17.04,17.10 + ver=0.0.0 + if ! distro_install_package snapd-xdg-open=$ver; then + echo "SKIP: cannot find snapd-xdg-open, skipping test" + exit 0 + fi + fi + + prevsnapdxdgver=$(dpkg-query --showformat='${Version}' --show snapd-xdg-open) + + # allow-downgrades prevents errors when new versions hit the archive, for instance, + # trying to install 2.11ubuntu1 over 2.11+0.16.04 + distro_install_local_package --allow-downgrades $GOHOME/snapd*.deb + + snapdxdgver=$(dpkg-query --showformat='${Version}' --show snapd-xdg-open) + [ "$snapdxdgver" != "$prevsnapdxdgver" ] diff --git a/tests/util/benchmark.sh b/tests/util/benchmark.sh new file mode 100755 index 00000000..48a94dec --- /dev/null +++ b/tests/util/benchmark.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +BACKEND="${1:-linode:}" +ITERATIONS=${2:-10} +OUTPUT_FILE="${3:-${PWD}/benchmark.out}" +SUCCESSFUL_EXECUTIONS=0 + +rm -f "$OUTPUT_FILE" + +for i in $(seq "$ITERATIONS"); do + echo "Running iteration $i of $ITERATIONS" + START_TIME=$SECONDS + if spread -v "$BACKEND"; then + SUCCESSFUL_EXECUTIONS=$((SUCCESSFUL_EXECUTIONS + 1)) + ITERATION_TIME=$((SECONDS - START_TIME)) + TOTAL_TIME=$((TOTAL_TIME + ITERATION_TIME)) + echo "$ITERATION_TIME" >> "$OUTPUT_FILE" + fi +done +echo "$SUCCESSFUL_EXECUTIONS successful executions out of $ITERATIONS" >> "$OUTPUT_FILE" +echo "Average: $(echo "scale=2; $TOTAL_TIME / $SUCCESSFUL_EXECUTIONS" | bc)s" >> "$OUTPUT_FILE" diff --git a/testutil/base.go b/testutil/base.go new file mode 100644 index 00000000..9d8712bc --- /dev/null +++ b/testutil/base.go @@ -0,0 +1,51 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package testutil + +import ( + "gopkg.in/check.v1" +) + +// BaseTest is a structure used as a base test suite for all the snappy +// tests. +type BaseTest struct { + cleanupHandlers []func() +} + +// SetUpTest prepares the cleanup +func (s *BaseTest) SetUpTest(c *check.C) { + s.cleanupHandlers = nil +} + +// TearDownTest cleans up the channel.ini files in case they were changed by +// the test. +// It also runs the cleanup handlers +func (s *BaseTest) TearDownTest(c *check.C) { + // run cleanup handlers and clear the slice + for _, f := range s.cleanupHandlers { + f() + } + s.cleanupHandlers = nil +} + +// AddCleanup adds a new cleanup function to the test +func (s *BaseTest) AddCleanup(f func()) { + s.cleanupHandlers = append(s.cleanupHandlers, f) +} diff --git a/testutil/checkers.go b/testutil/checkers.go new file mode 100644 index 00000000..4d2fcb56 --- /dev/null +++ b/testutil/checkers.go @@ -0,0 +1,152 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package testutil + +import ( + "fmt" + "reflect" + "strings" + + "gopkg.in/check.v1" +) + +type containsChecker struct { + *check.CheckerInfo +} + +// Contains is a Checker that looks for a elem in a container. +// The elem can be any object. The container can be an array, slice or string. +var Contains check.Checker = &containsChecker{ + &check.CheckerInfo{Name: "Contains", Params: []string{"container", "elem"}}, +} + +func commonEquals(container, elem interface{}, result *bool, error *string) bool { + containerV := reflect.ValueOf(container) + elemV := reflect.ValueOf(elem) + switch containerV.Kind() { + case reflect.Slice, reflect.Array, reflect.Map: + containerElemType := containerV.Type().Elem() + if containerElemType.Kind() == reflect.Interface { + // Ensure that element implements the type of elements stored in the container. + if !elemV.Type().Implements(containerElemType) { + *result = false + *error = fmt.Sprintf(""+ + "container has items of interface type %s but expected"+ + " element does not implement it", containerElemType) + return true + } + } else { + // Ensure that type of elements in container is compatible with elem + if containerElemType != elemV.Type() { + *result = false + *error = fmt.Sprintf( + "container has items of type %s but expected element is a %s", + containerElemType, elemV.Type()) + return true + } + } + case reflect.String: + // When container is a string, we expect elem to be a string as well + if elemV.Kind() != reflect.String { + *result = false + *error = fmt.Sprintf("element is a %T but expected a string", elem) + } else { + *result = strings.Contains(containerV.String(), elemV.String()) + *error = "" + } + return true + } + return false +} + +func (c *containsChecker) Check(params []interface{}, names []string) (result bool, error string) { + defer func() { + if v := recover(); v != nil { + result = false + error = fmt.Sprint(v) + } + }() + var container interface{} = params[0] + var elem interface{} = params[1] + if commonEquals(container, elem, &result, &error) { + return + } + // Do the actual test using == + switch containerV := reflect.ValueOf(container); containerV.Kind() { + case reflect.Slice, reflect.Array: + for length, i := containerV.Len(), 0; i < length; i++ { + itemV := containerV.Index(i) + if itemV.Interface() == elem { + return true, "" + } + } + return false, "" + case reflect.Map: + for _, keyV := range containerV.MapKeys() { + itemV := containerV.MapIndex(keyV) + if itemV.Interface() == elem { + return true, "" + } + } + return false, "" + default: + return false, fmt.Sprintf("%T is not a supported container", container) + } +} + +type deepContainsChecker struct { + *check.CheckerInfo +} + +// DeepContains is a Checker that looks for a elem in a container using +// DeepEqual. The elem can be any object. The container can be an array, slice +// or string. +var DeepContains check.Checker = &deepContainsChecker{ + &check.CheckerInfo{Name: "DeepContains", Params: []string{"container", "elem"}}, +} + +func (c *deepContainsChecker) Check(params []interface{}, names []string) (result bool, error string) { + var container interface{} = params[0] + var elem interface{} = params[1] + if commonEquals(container, elem, &result, &error) { + return + } + // Do the actual test using reflect.DeepEqual + switch containerV := reflect.ValueOf(container); containerV.Kind() { + case reflect.Slice, reflect.Array: + for length, i := containerV.Len(), 0; i < length; i++ { + itemV := containerV.Index(i) + if reflect.DeepEqual(itemV.Interface(), elem) { + return true, "" + } + } + return false, "" + case reflect.Map: + for _, keyV := range containerV.MapKeys() { + itemV := containerV.MapIndex(keyV) + if reflect.DeepEqual(itemV.Interface(), elem) { + return true, "" + } + } + return false, "" + default: + return false, fmt.Sprintf("%T is not a supported container", container) + } +} diff --git a/testutil/checkers_test.go b/testutil/checkers_test.go new file mode 100644 index 00000000..e7f3183f --- /dev/null +++ b/testutil/checkers_test.go @@ -0,0 +1,258 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +// 20160229: The tests with gccgo on powerpc fail for this file +// and it will loop endlessly. This is not reproducible +// with gccgo on amd64. Given that it's a relatively little +// used arch we disable the tests in here to workaround this +// gccgo bug. +// +build !ppc + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package testutil + +import ( + "reflect" + "runtime" + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + TestingT(t) +} + +type CheckersS struct{} + +var _ = Suite(&CheckersS{}) + +func testInfo(c *C, checker Checker, name string, paramNames []string) { + info := checker.Info() + if info.Name != name { + c.Fatalf("Got name %s, expected %s", info.Name, name) + } + if !reflect.DeepEqual(info.Params, paramNames) { + c.Fatalf("Got param names %#v, expected %#v", info.Params, paramNames) + } +} + +func testCheck(c *C, checker Checker, result bool, error string, params ...interface{}) ([]interface{}, []string) { + info := checker.Info() + if len(params) != len(info.Params) { + c.Fatalf("unexpected param count in test; expected %d got %d", len(info.Params), len(params)) + } + names := append([]string{}, info.Params...) + resultActual, errorActual := checker.Check(params, names) + if resultActual != result || errorActual != error { + c.Fatalf("%s.Check(%#v) returned (%#v, %#v) rather than (%#v, %#v)", + info.Name, params, resultActual, errorActual, result, error) + } + return params, names +} + +func (s *CheckersS) TestUnsupportedTypes(c *C) { + testInfo(c, Contains, "Contains", []string{"container", "elem"}) + testCheck(c, Contains, false, "int is not a supported container", 5, nil) + testCheck(c, Contains, false, "bool is not a supported container", false, nil) + testCheck(c, Contains, false, "element is a int but expected a string", "container", 1) +} + +func (s *CheckersS) TestContainsVerifiesTypes(c *C) { + testInfo(c, Contains, "Contains", []string{"container", "elem"}) + testCheck(c, Contains, + false, "container has items of type int but expected element is a string", + [...]int{1, 2, 3}, "foo") + testCheck(c, Contains, + false, "container has items of type int but expected element is a string", + []int{1, 2, 3}, "foo") + // This looks tricky, Contains looks at _values_, not at keys + testCheck(c, Contains, + false, "container has items of type int but expected element is a string", + map[string]int{"foo": 1, "bar": 2}, "foo") + testCheck(c, Contains, + false, "container has items of type int but expected element is a string", + map[string]int{"foo": 1, "bar": 2}, "foo") +} + +type animal interface { + Sound() string +} + +type dog struct{} + +func (d *dog) Sound() string { + return "bark" +} + +type cat struct{} + +func (c *cat) Sound() string { + return "meow" +} + +type tree struct{} + +func (s *CheckersS) TestContainsVerifiesInterfaceTypes(c *C) { + testCheck(c, Contains, + false, "container has items of interface type testutil.animal but expected element does not implement it", + [...]animal{&dog{}, &cat{}}, &tree{}) + testCheck(c, Contains, + false, "container has items of interface type testutil.animal but expected element does not implement it", + []animal{&dog{}, &cat{}}, &tree{}) + testCheck(c, Contains, + false, "container has items of interface type testutil.animal but expected element does not implement it", + map[string]animal{"dog": &dog{}, "cat": &cat{}}, &tree{}) +} + +func (s *CheckersS) TestContainsString(c *C) { + c.Assert("foo", Contains, "f") + c.Assert("foo", Contains, "fo") + c.Assert("foo", Not(Contains), "foobar") +} + +type myString string + +func (s *CheckersS) TestContainsCustomString(c *C) { + c.Assert(myString("foo"), Contains, myString("f")) + c.Assert(myString("foo"), Contains, myString("fo")) + c.Assert(myString("foo"), Not(Contains), myString("foobar")) + c.Assert("foo", Contains, myString("f")) + c.Assert("foo", Contains, myString("fo")) + c.Assert("foo", Not(Contains), myString("foobar")) + c.Assert(myString("foo"), Contains, "f") + c.Assert(myString("foo"), Contains, "fo") + c.Assert(myString("foo"), Not(Contains), "foobar") +} + +func (s *CheckersS) TestContainsArray(c *C) { + c.Assert([...]int{1, 2, 3}, Contains, 1) + c.Assert([...]int{1, 2, 3}, Contains, 2) + c.Assert([...]int{1, 2, 3}, Contains, 3) + c.Assert([...]int{1, 2, 3}, Not(Contains), 4) + c.Assert([...]animal{&dog{}, &cat{}}, Contains, &dog{}) + c.Assert([...]animal{&cat{}}, Not(Contains), &dog{}) +} + +func (s *CheckersS) TestContainsSlice(c *C) { + c.Assert([]int{1, 2, 3}, Contains, 1) + c.Assert([]int{1, 2, 3}, Contains, 2) + c.Assert([]int{1, 2, 3}, Contains, 3) + c.Assert([]int{1, 2, 3}, Not(Contains), 4) + c.Assert([]animal{&dog{}, &cat{}}, Contains, &dog{}) + c.Assert([]animal{&cat{}}, Not(Contains), &dog{}) +} + +func (s *CheckersS) TestContainsMap(c *C) { + c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, 1) + c.Assert(map[string]int{"foo": 1, "bar": 2}, Contains, 2) + c.Assert(map[string]int{"foo": 1, "bar": 2}, Not(Contains), 3) + c.Assert(map[string]animal{"dog": &dog{}, "cat": &cat{}}, Contains, &dog{}) + c.Assert(map[string]animal{"cat": &cat{}}, Not(Contains), &dog{}) +} + +// Arbitrary type that is not comparable +type myStruct struct { + attrs map[string]string +} + +func (s *CheckersS) TestContainsUncomparableType(c *C) { + if runtime.Compiler != "go" { + c.Skip("this test only works on go (not gccgo)") + } + + elem := myStruct{map[string]string{"k": "v"}} + containerArray := [...]myStruct{elem} + containerSlice := []myStruct{elem} + containerMap := map[string]myStruct{"foo": elem} + errMsg := "runtime error: comparing uncomparable type testutil.myStruct" + testInfo(c, Contains, "Contains", []string{"container", "elem"}) + testCheck(c, Contains, false, errMsg, containerArray, elem) + testCheck(c, Contains, false, errMsg, containerSlice, elem) + testCheck(c, Contains, false, errMsg, containerMap, elem) +} + +func (s *CheckersS) TestDeepContainsUnsupportedTypes(c *C) { + testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"}) + testCheck(c, DeepContains, false, "int is not a supported container", 5, nil) + testCheck(c, DeepContains, false, "bool is not a supported container", false, nil) + testCheck(c, DeepContains, false, "element is a int but expected a string", "container", 1) +} + +func (s *CheckersS) TestDeepContainsVerifiesTypes(c *C) { + testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"}) + testCheck(c, DeepContains, + false, "container has items of type int but expected element is a string", + [...]int{1, 2, 3}, "foo") + testCheck(c, DeepContains, + false, "container has items of type int but expected element is a string", + []int{1, 2, 3}, "foo") + // This looks tricky, DeepContains looks at _values_, not at keys + testCheck(c, DeepContains, + false, "container has items of type int but expected element is a string", + map[string]int{"foo": 1, "bar": 2}, "foo") +} + +func (s *CheckersS) TestDeepContainsString(c *C) { + c.Assert("foo", DeepContains, "f") + c.Assert("foo", DeepContains, "fo") + c.Assert("foo", Not(DeepContains), "foobar") +} + +func (s *CheckersS) TestDeepContainsCustomString(c *C) { + c.Assert(myString("foo"), DeepContains, myString("f")) + c.Assert(myString("foo"), DeepContains, myString("fo")) + c.Assert(myString("foo"), Not(DeepContains), myString("foobar")) + c.Assert("foo", DeepContains, myString("f")) + c.Assert("foo", DeepContains, myString("fo")) + c.Assert("foo", Not(DeepContains), myString("foobar")) + c.Assert(myString("foo"), DeepContains, "f") + c.Assert(myString("foo"), DeepContains, "fo") + c.Assert(myString("foo"), Not(DeepContains), "foobar") +} + +func (s *CheckersS) TestDeepContainsArray(c *C) { + c.Assert([...]int{1, 2, 3}, DeepContains, 1) + c.Assert([...]int{1, 2, 3}, DeepContains, 2) + c.Assert([...]int{1, 2, 3}, DeepContains, 3) + c.Assert([...]int{1, 2, 3}, Not(DeepContains), 4) +} + +func (s *CheckersS) TestDeepContainsSlice(c *C) { + c.Assert([]int{1, 2, 3}, DeepContains, 1) + c.Assert([]int{1, 2, 3}, DeepContains, 2) + c.Assert([]int{1, 2, 3}, DeepContains, 3) + c.Assert([]int{1, 2, 3}, Not(DeepContains), 4) +} + +func (s *CheckersS) TestDeepContainsMap(c *C) { + c.Assert(map[string]int{"foo": 1, "bar": 2}, DeepContains, 1) + c.Assert(map[string]int{"foo": 1, "bar": 2}, DeepContains, 2) + c.Assert(map[string]int{"foo": 1, "bar": 2}, Not(DeepContains), 3) +} + +func (s *CheckersS) TestDeepContainsUncomparableType(c *C) { + elem := myStruct{map[string]string{"k": "v"}} + containerArray := [...]myStruct{elem} + containerSlice := []myStruct{elem} + containerMap := map[string]myStruct{"foo": elem} + testInfo(c, DeepContains, "DeepContains", []string{"container", "elem"}) + testCheck(c, DeepContains, true, "", containerArray, elem) + testCheck(c, DeepContains, true, "", containerSlice, elem) + testCheck(c, DeepContains, true, "", containerMap, elem) +} diff --git a/testutil/dbustest.go b/testutil/dbustest.go new file mode 100644 index 00000000..17c70367 --- /dev/null +++ b/testutil/dbustest.go @@ -0,0 +1,72 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package testutil + +import ( + "fmt" + "os" + "os/exec" + + "github.com/godbus/dbus" + + . "gopkg.in/check.v1" +) + +// DBusTest provides a separate dbus session bus for running tests +type DBusTest struct { + tmpdir string + dbusDaemon *exec.Cmd + oldSessionBusEnv string + + // the dbus.Conn to the session bus that tests can use + SessionBus *dbus.Conn +} + +func (s *DBusTest) SetUpSuite(c *C) { + if _, err := exec.LookPath("dbus-daemon"); err != nil { + c.Skip(fmt.Sprintf("cannot run test without dbus-daemon: %s", err)) + return + } + if _, err := exec.LookPath("dbus-launch"); err != nil { + c.Skip(fmt.Sprintf("cannot run test without dbus-launch: %s", err)) + return + } + + s.tmpdir = c.MkDir() + s.dbusDaemon = exec.Command("dbus-daemon", "--session", fmt.Sprintf("--address=unix:%s/user_bus_socket", s.tmpdir)) + err := s.dbusDaemon.Start() + c.Assert(err, IsNil) + s.oldSessionBusEnv = os.Getenv("DBUS_SESSION_BUS_ADDRESS") + + s.SessionBus, err = dbus.SessionBus() + c.Assert(err, IsNil) +} + +func (s *DBusTest) TearDownSuite(c *C) { + os.Setenv("DBUS_SESSION_BUS_ADDRESS", s.oldSessionBusEnv) + if s.dbusDaemon != nil && s.dbusDaemon.Process != nil { + err := s.dbusDaemon.Process.Kill() + c.Assert(err, IsNil) + } + +} + +func (s *DBusTest) SetUpTest(c *C) {} +func (s *DBusTest) TearDownTest(c *C) {} diff --git a/testutil/exec.go b/testutil/exec.go new file mode 100644 index 00000000..3dd403f6 --- /dev/null +++ b/testutil/exec.go @@ -0,0 +1,139 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package testutil + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + + "gopkg.in/check.v1" +) + +// MockCmd allows mocking commands for testing. +type MockCmd struct { + binDir string + exeFile string + logFile string +} + +var scriptTpl = `#!/bin/sh +echo "$(basename "$0")" >> %[1]q +for arg in "$@"; do + echo "$arg" >> %[1]q +done +echo >> %[1]q +%s +` + +// MockCommand adds a mocked command. If the basename argument is a command +// it is added to PATH. If it is an absolute path it is just created there. +// the caller is responsible for the cleanup in this case. +// +// The command logs all invocations to a dedicated log file. If script is +// non-empty then it is used as is and the caller is responsible for how the +// script behaves (exit code and any extra behavior). If script is empty then +// the command exits successfully without any other side-effect. +func MockCommand(c *check.C, basename, script string) *MockCmd { + var binDir, exeFile, logFile string + if filepath.IsAbs(basename) { + binDir = filepath.Dir(basename) + exeFile = basename + logFile = basename + ".log" + } else { + binDir = c.MkDir() + exeFile = path.Join(binDir, basename) + logFile = path.Join(binDir, basename+".log") + os.Setenv("PATH", binDir+":"+os.Getenv("PATH")) + } + err := ioutil.WriteFile(exeFile, []byte(fmt.Sprintf(scriptTpl, logFile, script)), 0700) + if err != nil { + panic(err) + } + + return &MockCmd{binDir: binDir, exeFile: exeFile, logFile: logFile} +} + +// Also mock this command, using the same bindir and log +// Useful when you want to check the ordering of things. +func (cmd *MockCmd) Also(basename, script string) *MockCmd { + exeFile := path.Join(cmd.binDir, basename) + err := ioutil.WriteFile(exeFile, []byte(fmt.Sprintf(scriptTpl, cmd.logFile, script)), 0700) + if err != nil { + panic(err) + } + return &MockCmd{binDir: cmd.binDir, exeFile: exeFile, logFile: cmd.logFile} +} + +// Restore removes the mocked command from PATH +func (cmd *MockCmd) Restore() { + entries := strings.Split(os.Getenv("PATH"), ":") + for i, entry := range entries { + if entry == cmd.binDir { + entries = append(entries[:i], entries[i+1:]...) + break + } + } + os.Setenv("PATH", strings.Join(entries, ":")) +} + +// Calls returns a list of calls that were made to the mock command. +// of the form: +// [][]string{ +// {"cmd", "arg1", "arg2"}, // first invocation of "cmd" +// {"cmd", "arg1", "arg2"}, // second invocation of "cmd" +// } +func (cmd *MockCmd) Calls() [][]string { + raw, err := ioutil.ReadFile(cmd.logFile) + if os.IsNotExist(err) { + return nil + } + if err != nil { + panic(err) + } + logContent := strings.TrimSuffix(string(raw), "\n") + + allCalls := [][]string{} + calls := strings.Split(logContent, "\n\n") + for _, call := range calls { + call = strings.TrimSuffix(call, "\n") + allCalls = append(allCalls, strings.Split(call, "\n")) + } + return allCalls +} + +// ForgetCalls purges the list of calls made so far +func (cmd *MockCmd) ForgetCalls() { + err := os.Remove(cmd.logFile) + if os.IsNotExist(err) { + return + } + if err != nil { + panic(err) + } +} + +// BinDir returns the location of the directory holding overridden commands. +func (cmd *MockCmd) BinDir() string { + return cmd.binDir +} diff --git a/testutil/exec_test.go b/testutil/exec_test.go new file mode 100644 index 00000000..9d36c7ad --- /dev/null +++ b/testutil/exec_test.go @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package testutil + +import ( + "os/exec" + + . "gopkg.in/check.v1" +) + +type mockCommandSuite struct{} + +var _ = Suite(&mockCommandSuite{}) + +func (s *mockCommandSuite) TestMockCommand(c *C) { + mock := MockCommand(c, "cmd", "true") + defer mock.Restore() + err := exec.Command("cmd", "first-run", "--arg1", "arg2", "a space").Run() + c.Assert(err, IsNil) + err = exec.Command("cmd", "second-run", "--arg1", "arg2", "a %s").Run() + c.Assert(err, IsNil) + c.Assert(mock.Calls(), DeepEquals, [][]string{ + {"cmd", "first-run", "--arg1", "arg2", "a space"}, + {"cmd", "second-run", "--arg1", "arg2", "a %s"}, + }) +} + +func (s *mockCommandSuite) TestMockCommandAlso(c *C) { + mock := MockCommand(c, "fst", "") + also := mock.Also("snd", "") + defer mock.Restore() + + c.Assert(exec.Command("fst").Run(), IsNil) + c.Assert(exec.Command("snd").Run(), IsNil) + c.Check(mock.Calls(), DeepEquals, [][]string{{"fst"}, {"snd"}}) + c.Check(mock.Calls(), DeepEquals, also.Calls()) +} diff --git a/timeout/timeout.go b/timeout/timeout.go new file mode 100644 index 00000000..265ae64c --- /dev/null +++ b/timeout/timeout.go @@ -0,0 +1,76 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package timeout + +import ( + "encoding/json" + "time" +) + +// Timeout is a time.Duration that knows how to roundtrip to json and yaml +type Timeout time.Duration + +// DefaultTimeout specifies the timeout for services that do not specify StopTimeout +var DefaultTimeout = Timeout(30 * time.Second) + +// MarshalJSON is from the json.Marshaler interface +func (t Timeout) MarshalJSON() ([]byte, error) { + return json.Marshal(t.String()) +} + +// UnmarshalJSON is from the json.Unmarshaler interface +func (t *Timeout) UnmarshalJSON(buf []byte) error { + var str string + if err := json.Unmarshal(buf, &str); err != nil { + return err + } + + dur, err := time.ParseDuration(str) + if err != nil { + return err + } + + *t = Timeout(dur) + + return nil +} + +func (t *Timeout) UnmarshalYAML(unmarshal func(interface{}) error) error { + var str string + if err := unmarshal(&str); err != nil { + return err + } + dur, err := time.ParseDuration(str) + if err != nil { + return err + } + *t = Timeout(dur) + return nil +} + +// String returns a string representing the duration +func (t Timeout) String() string { + return time.Duration(t).String() +} + +// Seconds returns the duration as a floating point number of seconds. +func (t Timeout) Seconds() float64 { + return time.Duration(t).Seconds() +} diff --git a/timeout/timeout_test.go b/timeout/timeout_test.go new file mode 100644 index 00000000..e58a6e0f --- /dev/null +++ b/timeout/timeout_test.go @@ -0,0 +1,65 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package timeout + +import ( + "encoding/json" + "testing" + "time" + + . "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type TimeoutTestSuite struct { +} + +var _ = Suite(&TimeoutTestSuite{}) + +func (s *TimeoutTestSuite) TestTimeoutMarshal(c *C) { + bs, err := Timeout(DefaultTimeout).MarshalJSON() + c.Assert(err, IsNil) + c.Check(string(bs), Equals, `"30s"`) +} + +type testT struct { + T Timeout `yaml:"T"` +} + +func (s *TimeoutTestSuite) TestTimeoutMarshalIndirect(c *C) { + bs, err := json.Marshal(testT{DefaultTimeout}) + c.Assert(err, IsNil) + c.Check(string(bs), Equals, `{"T":"30s"}`) +} + +func (s *TimeoutTestSuite) TestTimeoutUnmarshalJSON(c *C) { + var t testT + c.Assert(json.Unmarshal([]byte(`{"T": "17ms"}`), &t), IsNil) + c.Check(t, DeepEquals, testT{T: Timeout(17 * time.Millisecond)}) +} + +func (s *TimeoutTestSuite) TestTimeoutUnmarshalYAML(c *C) { + var t testT + c.Assert(yaml.Unmarshal([]byte(`T: 17ms`), &t), IsNil) + c.Check(t, DeepEquals, testT{T: Timeout(17 * time.Millisecond)}) +} diff --git a/timeutil/export_test.go b/timeutil/export_test.go new file mode 100644 index 00000000..cfd30fd9 --- /dev/null +++ b/timeutil/export_test.go @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package timeutil + +import "time" + +func MockTimeNow(f func() time.Time) (restorer func()) { + origTimeNow := timeNow + timeNow = f + return func() { timeNow = origTimeNow } +} diff --git a/timeutil/schedule.go b/timeutil/schedule.go new file mode 100644 index 00000000..1e593613 --- /dev/null +++ b/timeutil/schedule.go @@ -0,0 +1,235 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package timeutil + +import ( + "fmt" + "math/rand" + "regexp" + "strconv" + "strings" + "time" +) + +var validTime = regexp.MustCompile(`^([0-9]|0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$`) + +type TimeOfDay struct { + Hour int + Minute int +} + +func (t TimeOfDay) String() string { + return fmt.Sprintf("%02d:%02d", t.Hour, t.Minute) +} + +// Sub subtracts `other` TimeOfDay from current and returns duration +func (t TimeOfDay) Sub(other TimeOfDay) time.Duration { + t1 := time.Duration(t.Hour)*time.Hour + time.Duration(t.Minute)*time.Minute + t2 := time.Duration(other.Hour)*time.Hour + time.Duration(other.Minute)*time.Minute + return t1 - t2 +} + +// Add adds given duration and returns a new TimeOfDay +func (t TimeOfDay) Add(dur time.Duration) TimeOfDay { + t1 := time.Duration(t.Hour)*time.Hour + time.Duration(t.Minute)*time.Minute + t2 := t1 + dur + nt := TimeOfDay{ + Hour: int(t2.Hours()) % 24, + Minute: int(t2.Minutes()) % 60, + } + return nt +} + +// IsValidWeekday returns true if given s looks like a valid weekday. Valid +// inputs are 3 letter, lowercase abbreviations of week days. +func IsValidWeekday(s string) bool { + _, ok := weekdayMap[s] + return ok +} + +// ParseTime parses a string that contains hour:minute and returns +// an TimeOfDay type or an error +func ParseTime(s string) (t TimeOfDay, err error) { + m := validTime.FindStringSubmatch(s) + if len(m) == 0 { + return t, fmt.Errorf("cannot parse %q", s) + } + t.Hour, err = strconv.Atoi(m[1]) + if err != nil { + return t, fmt.Errorf("cannot parse %q: %s", m[1], err) + } + t.Minute, err = strconv.Atoi(m[2]) + if err != nil { + return t, fmt.Errorf("cannot parse %q: %s", m[2], err) + } + return t, nil +} + +// Schedule defines a start and end time in which events should run. +type Schedule struct { + Start TimeOfDay + End TimeOfDay + + Weekday string +} + +func (sched *Schedule) String() string { + return fmt.Sprintf("%s-%s", sched.Start, sched.End) +} + +func (sched *Schedule) Next(last time.Time) (start, end time.Time) { + now := timeNow() + + t := last + for { + a := time.Date(t.Year(), t.Month(), t.Day(), sched.Start.Hour, sched.Start.Minute, 0, 0, time.Local) + b := time.Date(t.Year(), t.Month(), t.Day(), sched.End.Hour, sched.End.Minute, 0, 0, time.Local) + + // not using AddDate() here as this can panic() if no + // location is set + t = t.Add(24 * time.Hour) + + // same inteval as last update, move forward + if (last.Equal(a) || last.After(a)) && (last.Equal(b) || last.Before(b)) { + continue + } + if b.Before(now) { + continue + } + + return a, b + } +} + +func randDur(a, b time.Time) time.Duration { + dur := b.Sub(a) + if dur > 5*time.Minute { + // doing it this way we still spread really small windows about + dur -= 5 * time.Minute + } + + if dur <= 0 { + // avoid panic'ing (even if things are probably messed up) + return 0 + } + + return time.Duration(rand.Int63n(int64(dur))) +} + +var ( + timeNow = time.Now + + // FIMXE: pass in as a parameter for next + maxDuration = 14 * 24 * time.Hour +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// Next will return the duration until a random time in the next +// schedule window. +func Next(schedule []*Schedule, last time.Time) time.Duration { + now := timeNow() + + a := last.Add(maxDuration) + b := a.Add(1 * time.Hour) + for _, sched := range schedule { + start, end := sched.Next(last) + if start.Before(a) { + a = start + b = end + } + } + if a.Before(now) { + return 0 + } + + when := a.Sub(now) + randDur(a, b) + + return when +} + +var weekdayMap = map[string]int{ + "sun": 0, + "mon": 1, + "tue": 2, + "wed": 3, + "thu": 4, + "fri": 5, + "sat": 6, +} + +// parseTimeInterval gets an input like "9:00-11:00" +// and extracts the start and end of that schedule string and +// returns them and any errors. +func parseTimeInterval(s string) (start, end TimeOfDay, err error) { + l := strings.SplitN(s, "-", 2) + if len(l) != 2 { + return start, end, fmt.Errorf("cannot parse %q: not a valid interval", s) + } + + start, err = ParseTime(l[0]) + if err != nil { + return start, end, fmt.Errorf("cannot parse %q: not a valid time", l[0]) + } + end, err = ParseTime(l[1]) + if err != nil { + return start, end, fmt.Errorf("cannot parse %q: not a valid time", l[1]) + } + if !(start.Hour <= end.Hour && start.Minute <= end.Minute) { + return start, end, fmt.Errorf("cannot parse %q: time in an interval cannot go backwards", s) + } + + return start, end, nil +} + +// parseSingleSchedule parses a schedule string like "9:00-11:00" +func parseSingleSchedule(s string) (*Schedule, error) { + start, end, err := parseTimeInterval(s) + if err != nil { + return nil, err + } + + return &Schedule{ + Start: start, + End: end, + }, nil +} + +// ParseSchedule takes a schedule string in the form of: +// +// 9:00-15:00 (every day between 9am and 3pm) +// 9:00-15:00/21:00-22:00 (every day between 9am,5pm and 9pm,10pm) +// +// and returns a list of Schedule types or an error +func ParseSchedule(scheduleSpec string) ([]*Schedule, error) { + var schedule []*Schedule + + for _, s := range strings.Split(scheduleSpec, "/") { + sched, err := parseSingleSchedule(s) + if err != nil { + return nil, err + } + schedule = append(schedule, sched) + } + + return schedule, nil +} diff --git a/timeutil/schedule_test.go b/timeutil/schedule_test.go new file mode 100644 index 00000000..e5cbac0c --- /dev/null +++ b/timeutil/schedule_test.go @@ -0,0 +1,242 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package timeutil_test + +import ( + "strings" + "testing" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/timeutil" +) + +func Test(t *testing.T) { TestingT(t) } + +type timeutilSuite struct{} + +var _ = Suite(&timeutilSuite{}) + +func (ts *timeutilSuite) TestTimeOfDay(c *C) { + td := timeutil.TimeOfDay{Hour: 23, Minute: 59} + c.Check(td.Add(time.Minute), Equals, timeutil.TimeOfDay{Hour: 0, Minute: 0}) + + td = timeutil.TimeOfDay{Hour: 5, Minute: 34} + c.Check(td.Add(time.Minute), Equals, timeutil.TimeOfDay{Hour: 5, Minute: 35}) + + td = timeutil.TimeOfDay{Hour: 10, Minute: 1} + c.Check(td.Sub(timeutil.TimeOfDay{Hour: 10, Minute: 0}), Equals, time.Minute) +} + +func (ts *timeutilSuite) TestParseTimeOfDay(c *C) { + for _, t := range []struct { + timeStr string + hour, minute int + errStr string + }{ + {"8:59", 8, 59, ""}, + {"08:59", 8, 59, ""}, + {"12:00", 12, 0, ""}, + {"xx", 0, 0, `cannot parse "xx"`}, + {"11:61", 0, 0, `cannot parse "11:61"`}, + {"25:00", 0, 0, `cannot parse "25:00"`}, + } { + ti, err := timeutil.ParseTime(t.timeStr) + if t.errStr != "" { + c.Check(err, ErrorMatches, t.errStr) + } else { + c.Check(err, IsNil) + c.Check(ti.Hour, Equals, t.hour) + c.Check(ti.Minute, Equals, t.minute) + } + } +} + +func (ts *timeutilSuite) TestScheduleString(c *C) { + for _, t := range []struct { + sched timeutil.Schedule + str string + }{ + {timeutil.Schedule{Start: timeutil.TimeOfDay{Hour: 13, Minute: 41}, End: timeutil.TimeOfDay{Hour: 14, Minute: 59}}, "13:41-14:59"}, + } { + c.Check(t.sched.String(), Equals, t.str) + } +} + +func (ts *timeutilSuite) TestParseSchedule(c *C) { + for _, t := range []struct { + in string + expected []*timeutil.Schedule + errStr string + }{ + // invalid + {"", nil, `cannot parse "": not a valid interval`}, + {"invalid-11:00", nil, `cannot parse "invalid": not a valid time`}, + {"9:00-11:00/invalid", nil, `cannot parse "invalid": not a valid interval`}, + {"09:00-25:00", nil, `cannot parse "25:00": not a valid time`}, + // moving backwards + {"11:00-09:00", nil, `cannot parse "11:00-09:00": time in an interval cannot go backwards`}, + {"23:00-01:00", nil, `cannot parse "23:00-01:00": time in an interval cannot go backwards`}, + + // valid + {"9:00-11:00", []*timeutil.Schedule{{Start: timeutil.TimeOfDay{Hour: 9}, End: timeutil.TimeOfDay{Hour: 11}}}, ""}, + {"9:00-11:00/20:00-22:00", []*timeutil.Schedule{{Start: timeutil.TimeOfDay{Hour: 9}, End: timeutil.TimeOfDay{Hour: 11}}, {Start: timeutil.TimeOfDay{Hour: 20}, End: timeutil.TimeOfDay{Hour: 22}}}, ""}, + } { + schedule, err := timeutil.ParseSchedule(t.in) + if t.errStr != "" { + c.Check(err, ErrorMatches, t.errStr, Commentf("%q returned unexpected error: %s", t.in, err)) + } else { + c.Check(err, IsNil, Commentf("%q returned error: %s", t.in, err)) + c.Check(schedule, DeepEquals, t.expected, Commentf("%q failed", t.in)) + } + + } +} + +func (ts *timeutilSuite) TestIsValidWeekday(c *C) { + for _, t := range []struct { + in string + expected bool + }{ + {"mon", true}, + {"tue", true}, + {"wed", true}, + {"thu", true}, + {"fri", true}, + {"sat", true}, + {"sun", true}, + {"foo", false}, + {"bar", false}, + {"barsatfu", false}, + } { + c.Check(t.expected, Equals, timeutil.IsValidWeekday(t.in), + Commentf("%q returned unexpected value for, expected %v", t.in, t.expected)) + } +} + +func parse(c *C, s string) (time.Duration, time.Duration) { + l := strings.Split(s, "-") + c.Assert(l, HasLen, 2) + a, err := time.ParseDuration(l[0]) + c.Assert(err, IsNil) + b, err := time.ParseDuration(l[1]) + c.Assert(err, IsNil) + return a, b +} + +func (ts *timeutilSuite) TestScheduleNext(c *C) { + const shortForm = "2006-01-02 15:04" + + for _, t := range []struct { + schedule string + last string + now string + next string + }{ + { + // daily schedule, missed one window + // -> run next daily window + schedule: "9:00-11:00/21:00-23:00", + last: "2017-02-05 22:00", + now: "2017-02-06 20:00", + next: "1h-3h", + }, + { + // daily schedule, used one window + // -> run next daily window + schedule: "9:00-11:00/21:00-23:00", + last: "2017-02-06 10:00", + now: "2017-02-06 20:00", + next: "1h-3h", + }, + { + // daily schedule, missed all todays windows + // run tomorrow + schedule: "9:00-11:00/21:00-22:00", + last: "2017-02-04 21:30", + now: "2017-02-06 23:00", + next: "10h-12h", + }, + { + // single daily schedule, already updated today + schedule: "9:00-11:00", + last: "2017-02-06 09:30", + now: "2017-02-06 10:00", + next: "23h-25h", + }, + { + // single daily schedule, already updated today + // (at exactly the edge) + schedule: "9:00-11:00", + last: "2017-02-06 09:00", + now: "2017-02-06 09:00", + next: "24h-26h", + }, + { + // single daily schedule, last update a day ago + // now is within the update window so randomize + // (run within remaining time delta) + schedule: "9:00-11:00", + last: "2017-02-05 09:30", + now: "2017-02-06 10:00", + next: "0-55m", + }, + { + // multi daily schedule, already updated today + schedule: "9:00-11:00/21:00-22:00", + last: "2017-02-06 21:30", + now: "2017-02-06 23:00", + next: "10h-12h", + }, + { + // daily schedule, very small window + schedule: "9:00-9:03", + last: "2017-02-05 09:02", + now: "2017-02-06 08:58", + next: "2m-5m", + }, + { + // daily schedule, zero window + schedule: "9:00-9:00", + last: "2017-02-05 09:02", + now: "2017-02-06 08:58", + next: "2m-2m", + }, + } { + last, err := time.ParseInLocation(shortForm, t.last, time.Local) + c.Assert(err, IsNil) + + fakeNow, err := time.ParseInLocation(shortForm, t.now, time.Local) + c.Assert(err, IsNil) + restorer := timeutil.MockTimeNow(func() time.Time { + return fakeNow + }) + defer restorer() + + sched, err := timeutil.ParseSchedule(t.schedule) + c.Assert(err, IsNil) + minDist, maxDist := parse(c, t.next) + + next := timeutil.Next(sched, last) + c.Check(next >= minDist && next <= maxDist, Equals, true, Commentf("invalid distance for schedule %q with last refresh %q, now %q, expected %v, got %v", t.schedule, t.last, t.now, t.next, next)) + } + +} diff --git a/update-pot b/update-pot new file mode 100755 index 00000000..c3db0bc9 --- /dev/null +++ b/update-pot @@ -0,0 +1,39 @@ +#!/bin/sh +# -*- Mode: sh; indent-tabs-mode: t -*- + +set -e + +HERE="$(dirname "$0")" + +OUTPUT="$HERE/po/snappy.pot" +if [ -n "$1" ]; then + OUTPUT="$1" +fi + +# ensure we have our xgettext-go +go install github.com/snapcore/snapd/i18n/xgettext-go + +find "$HERE" -name "*.go" -type f -print0 | xargs -0 \ + "${GOPATH%%:*}/bin/xgettext-go" \ + -o "$OUTPUT" \ + --add-comments-tag=TRANSLATORS: \ + --no-location \ + --sort-output \ + --package-name=snappy\ + --msgid-bugs-address=snappy-devel@lists.ubuntu.com \ + --keyword=i18n.G \ + --keyword-plural=i18n.DG + +#xgettext -d snappy -o "$OUTPUT" --c++ --from-code=UTF-8 \ +# --indent --add-comments=TRANSLATORS: --no-location --sort-output \ +# --package-name=snappy \ +# --msgid-bugs-address=snappy-devel@lists.ubuntu.com \ +# --keyword=NG:1,2 --keyword=G \ +# $HERE/*/*.go $HERE/cmd/*/*.go + +# language packs +for p in ${HERE}/po/*.po; do + lang=$(basename "$p" .po) + mkdir -p "$HERE/share/locale/$lang/LC_MESSAGES" + msgfmt -v -o "$HERE/share/locale/$lang/LC_MESSAGES/snappy.mo" "$p" +done diff --git a/userd/launcher.go b/userd/launcher.go new file mode 100644 index 00000000..fc427e87 --- /dev/null +++ b/userd/launcher.go @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package userd + +import ( + "fmt" + "net/url" + "os/exec" + + "github.com/godbus/dbus" + "github.com/snapcore/snapd/strutil" +) + +const launcherIntrospectionXML = ` + + + + + + + + + + + +` + +var ( + allowedURLSchemes = []string{"http", "https", "mailto"} +) + +// Launcher implements the 'io.snapcraft.Launcher' DBus interface. +type Launcher struct{} + +// Name returns the name of the interface this object implements +func (s *Launcher) Name() string { + return "io.snapcraft.Launcher" +} + +// IntrospectionData gives the XML formatted introspection description +// of the DBus service. +func (s *Launcher) IntrospectionData() string { + return launcherIntrospectionXML +} + +func makeAccessDeniedError(err error) *dbus.Error { + return &dbus.Error{ + Name: "org.freedesktop.DBus.Error.AccessDenied", + Body: []interface{}{err.Error()}, + } +} + +// OpenURL implements the 'OpenURL' method of the 'com.canonical.Launcher' +// DBus interface. Before the provided url is passed to xdg-open the scheme is +// validated against a list of allowed schemes. All other schemes are denied. +func (s *Launcher) OpenURL(addr string) *dbus.Error { + u, err := url.Parse(addr) + if err != nil { + return &dbus.ErrMsgInvalidArg + } + + if !strutil.ListContains(allowedURLSchemes, u.Scheme) { + return makeAccessDeniedError(fmt.Errorf("Supplied URL scheme %q is not allowed", u.Scheme)) + } + + if err = exec.Command("xdg-open", addr).Run(); err != nil { + return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL")) + } + + return nil +} diff --git a/userd/launcher_test.go b/userd/launcher_test.go new file mode 100644 index 00000000..acc17424 --- /dev/null +++ b/userd/launcher_test.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package userd_test + +import ( + "github.com/godbus/dbus" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/userd" +) + +type launcherSuite struct { + launcher *userd.Launcher + + mockXdgOpen *testutil.MockCmd +} + +var _ = Suite(&launcherSuite{}) + +func (s *launcherSuite) SetUpTest(c *C) { + s.launcher = &userd.Launcher{} + s.mockXdgOpen = testutil.MockCommand(c, "xdg-open", "") +} + +func (s *launcherSuite) TearDownTest(c *C) { + s.mockXdgOpen.Restore() +} + +func (s *launcherSuite) TestOpenURLWithNotAllowedScheme(c *C) { + for _, t := range []struct { + url string + errMatcher string + }{ + {"tel://049112233445566", "Supplied URL scheme \"tel\" is not allowed"}, + {"aabbccdd0011", "Supplied URL scheme \"\" is not allowed"}, + {"invälid:%url", dbus.ErrMsgInvalidArg.Error()}, + } { + err := s.launcher.OpenURL(t.url) + c.Assert(err, ErrorMatches, t.errMatcher) + c.Assert(s.mockXdgOpen.Calls(), IsNil) + } +} + +func (s *launcherSuite) TestOpenURLWithAllowedSchemeHappy(c *C) { + for _, schema := range []string{"http", "https", "mailto"} { + err := s.launcher.OpenURL(schema + "://snapcraft.io") + c.Assert(err, IsNil) + c.Assert(s.mockXdgOpen.Calls(), DeepEquals, [][]string{ + {"xdg-open", schema + "://snapcraft.io"}, + }) + s.mockXdgOpen.ForgetCalls() + } +} + +func (s *launcherSuite) TestOpenURLWithFailingXdgOpen(c *C) { + cmd := testutil.MockCommand(c, "xdg-open", "false") + defer cmd.Restore() + + err := s.launcher.OpenURL("https://snapcraft.io") + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "cannot open supplied URL") +} diff --git a/userd/userd.go b/userd/userd.go new file mode 100644 index 00000000..870ac1a4 --- /dev/null +++ b/userd/userd.go @@ -0,0 +1,113 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package userd + +import ( + "bytes" + "fmt" + + "github.com/godbus/dbus" + "github.com/godbus/dbus/introspect" + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/logger" +) + +const ( + busName = "io.snapcraft.Launcher" + basePath = "/io/snapcraft/Launcher" +) + +type dbusInterface interface { + Name() string + IntrospectionData() string +} + +type Userd struct { + tomb tomb.Tomb + conn *dbus.Conn + dbusIfaces []dbusInterface +} + +func (ud *Userd) createAndExportInterfaces() { + ud.dbusIfaces = []dbusInterface{&Launcher{}} + + var buffer bytes.Buffer + buffer.WriteString("") + + for _, iface := range ud.dbusIfaces { + ud.conn.Export(iface, basePath, iface.Name()) + buffer.WriteString(iface.IntrospectionData()) + } + + buffer.WriteString(introspect.IntrospectDataString) + buffer.WriteString("") + + ud.conn.Export(introspect.Introspectable(buffer.String()), basePath, "org.freedesktop.DBus.Introspectable") +} + +func (ud *Userd) Init() error { + var err error + + ud.conn, err = dbus.SessionBus() + if err != nil { + return err + } + + reply, err := ud.conn.RequestName(busName, dbus.NameFlagDoNotQueue) + if err != nil { + return err + } + + if reply != dbus.RequestNameReplyPrimaryOwner { + err = fmt.Errorf("cannot obtain bus name '%s'", busName) + return err + } + + ud.createAndExportInterfaces() + return nil +} + +func (ud *Userd) Start() { + logger.Noticef("Starting snap userd") + + ud.tomb.Go(func() error { + // Listen to keep our thread up and running. All DBus bits + // are running in the background + select { + case <-ud.tomb.Dying(): + ud.conn.Close() + } + err := ud.tomb.Err() + if err != nil && err != tomb.ErrStillAlive { + return err + } + return nil + }) +} + +func (ud *Userd) Stop() error { + ud.tomb.Kill(nil) + return ud.tomb.Wait() +} + +func (ud *Userd) Dying() <-chan struct{} { + return ud.tomb.Dying() +} diff --git a/vendor/vendor.json b/vendor/vendor.json new file mode 100644 index 00000000..4045d9a7 --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,197 @@ +{ + "comment": "", + "ignore": "test", + "package": [ + { + "path": "context", + "revision": "" + }, + { + "checksumSHA1": "RBwpnMpfQt7Jo7YWrRph0Vwe+f0=", + "path": "github.com/coreos/go-systemd/activation", + "revision": "48702e0da86bd25e76cfef347e2adeb434a0d0a6", + "revisionTime": "2016-11-14T12:22:54Z" + }, + { + "checksumSHA1": "h77tT8kVh8x/J5ikkZReONPUjU0=", + "path": "github.com/godbus/dbus", + "revision": "97646858c46433e4afb3432ad28c12e968efa298", + "revisionTime": "2017-08-22T15:24:03Z" + }, + { + "checksumSHA1": "NrP46FPoALgKz3FY6puL3syMAAI=", + "path": "github.com/godbus/dbus/introspect", + "revision": "97646858c46433e4afb3432ad28c12e968efa298", + "revisionTime": "2017-08-22T15:24:03Z" + }, + { + "checksumSHA1": "iIUYZyoanCQQTUaWsu8b+iOSPt4=", + "path": "github.com/gorilla/context", + "revision": "1c83b3eabd45b6d76072b66b746c20815fb2872d", + "revisionTime": "2015-08-20T05:12:45Z" + }, + { + "checksumSHA1": "xF+WqeL9eAlbUkiO2bLghMQzO4k=", + "path": "github.com/gorilla/mux", + "revision": "0eeaf8392f5b04950925b8a69fe70f110fa7cbfc", + "revisionTime": "2016-03-17T21:34:30Z" + }, + { + "checksumSHA1": "Ihm00CfTHuuTYXgXJkgH7TFP0L4=", + "path": "github.com/jessevdk/go-flags", + "revision": "96dc06278ce32a0e9d957d590bb987c81ee66407", + "revisionTime": "2017-07-20T12:40:56Z" + }, + { + "checksumSHA1": "bzUdFxQ29mPK0lwgFVcF0GFN74Q=", + "path": "github.com/mvo5/goconfigparser", + "revision": "26426272dda20cc76aa1fa44286dc743d2972fe8", + "revisionTime": "2015-02-12T09:37:50Z" + }, + { + "checksumSHA1": "EoJqr2ZG7jODFsCnKwrn4JWRS+Y=", + "path": "github.com/mvo5/libseccomp-golang", + "revision": "84e1d1c75beaa58be6a76d2fc94d95eb8c1583b6", + "revisionTime": "2017-06-14T13:46:31Z" + }, + { + "checksumSHA1": "lG6diF/yE9cGgQIKRAlsaeYAjO4=", + "path": "github.com/ojii/gettext.go", + "revision": "95289a7e0ac17c76737a5ceca3c9471c0adf70c7", + "revisionTime": "2016-07-14T06:47:45Z" + }, + { + "checksumSHA1": "j5FytTwC2nAqSrDR7V7O7E4cHKM=", + "path": "github.com/ojii/gettext.go/pluralforms", + "revision": "95289a7e0ac17c76737a5ceca3c9471c0adf70c7", + "revisionTime": "2016-07-14T06:47:45Z" + }, + { + "checksumSHA1": "TT1rac6kpQp2vz24m5yDGUNQ/QQ=", + "path": "golang.org/x/crypto/cast5", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d" + }, + { + "checksumSHA1": "Y/FcWB2/xSfX1rRp7HYhktHNw8s=", + "path": "golang.org/x/crypto/nacl/secretbox", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-06-29T04:06:47Z" + }, + { + "checksumSHA1": "olOKkhrdkYQHZ0lf1orrFQPQrv4=", + "path": "golang.org/x/crypto/openpgp/armor", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-06-29T04:06:47Z" + }, + { + "checksumSHA1": "eo/KtdjieJQXH7Qy+faXFcF70ME=", + "path": "golang.org/x/crypto/openpgp/elgamal", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-06-29T04:06:47Z" + }, + { + "checksumSHA1": "rlxVSaGgqdAgwblsErxTxIfuGfg=", + "path": "golang.org/x/crypto/openpgp/errors", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-06-29T04:06:47Z" + }, + { + "checksumSHA1": "LWdaR8Q9yn6eBCcnGl0HvJRDUBE=", + "path": "golang.org/x/crypto/openpgp/packet", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-06-29T04:06:47Z" + }, + { + "checksumSHA1": "s2qT4UwvzBSkzXuiuMkowif1Olw=", + "path": "golang.org/x/crypto/openpgp/s2k", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-06-29T04:06:47Z" + }, + { + "checksumSHA1": "kVKE0OX1Xdw5mG7XKT86DLLKE2I=", + "path": "golang.org/x/crypto/poly1305", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-07-23T04:49:35Z" + }, + { + "checksumSHA1": "RUr1Owi1/hTpuAqKeeus6YuEyz8=", + "path": "golang.org/x/crypto/salsa20/salsa", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-07-23T04:49:35Z" + }, + { + "checksumSHA1": "DDHnuGCrmkKSXdNzc8pmn6P5O28=", + "path": "golang.org/x/crypto/sha3", + "revision": "5ef0053f77724838734b6945dd364d3847e5de1d", + "revisionTime": "2017-06-29T04:06:47Z" + }, + { + "checksumSHA1": "9C4Av3ypK5pi173F76ogJT/d8x4=", + "comment": "using revision before x/sys/unix/ was added to allow building on powerpc", + "path": "golang.org/x/crypto/ssh/terminal", + "revision": "a19fa444682e099bed1a53260e1d755754cd098a", + "revisionTime": "2015-11-02T21:41:26Z" + }, + { + "checksumSHA1": "Y+HGqEkYM15ir+J93MEaHdyFy0c=", + "path": "golang.org/x/net/context", + "revision": "c81e7f25cb61200d8bf0ae971a0bac8cb638d5bc", + "revisionTime": "2017-06-28T23:42:41Z" + }, + { + "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=", + "path": "golang.org/x/net/context/ctxhttp", + "revision": "c81e7f25cb61200d8bf0ae971a0bac8cb638d5bc", + "revisionTime": "2017-06-28T23:42:41Z" + }, + { + "checksumSHA1": "CEFTYXtWmgSh+3Ik1NmDaJcz4E0=", + "path": "gopkg.in/check.v1", + "revision": "20d25e2804050c1cd24a7eea1e7a6447dd0e74ec", + "revisionTime": "2016-12-08T18:13:25Z" + }, + { + "checksumSHA1": "mWrTCVjXPLevlqKUqpzSoEM3tu8=", + "path": "gopkg.in/macaroon.v1", + "revision": "ab3940c6c16510a850e1c2dd628b919f0f3f1464", + "revisionTime": "2015-01-21T11:42:31Z" + }, + { + "checksumSHA1": "YsB2DChSV9HxdzHaKATllAUKWSI=", + "path": "gopkg.in/mgo.v2/bson", + "revision": "3f83fa5005286a7fe593b055f0d7771a7dce4655", + "revisionTime": "2016-08-18T02:01:20Z" + }, + { + "checksumSHA1": "XQsrqoNT1U0KzLxOFcAZVvqhLfk=", + "path": "gopkg.in/mgo.v2/internal/json", + "revision": "3f83fa5005286a7fe593b055f0d7771a7dce4655", + "revisionTime": "2016-08-18T02:01:20Z" + }, + { + "checksumSHA1": "lBMMakT63h9ywP4d5wlkhOYMCAs=", + "path": "gopkg.in/retry.v1", + "revision": "c09f6b86ba4d5d2cf5bdf0665364aec9fd4815db", + "revisionTime": "2016-10-25T18:07:18Z" + }, + { + "checksumSHA1": "t95/FNK2nGCx3e6IARbO7OrcCuY=", + "path": "gopkg.in/tomb.v2", + "revision": "d5d1b5820637886def9eef33e03a27a9f166942c", + "revisionTime": "2016-12-08T15:16:19Z" + }, + { + "checksumSHA1": "hiOUviIMDKo7y918uBiEhfJyOkk=", + "path": "gopkg.in/tylerb/graceful.v1", + "revision": "50a48b6e73fcc75b45e22c05b79629a67c79e938", + "revisionTime": "2016-08-29T01:00:30Z" + }, + { + "checksumSHA1": "fALlQNY1fM99NesfLJ50KguWsio=", + "path": "gopkg.in/yaml.v2", + "revision": "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b", + "revisionTime": "2017-04-07T17:21:22Z" + } + ], + "rootPath": "github.com/snapcore/snapd" +} diff --git a/wrappers/binaries.go b/wrappers/binaries.go new file mode 100644 index 00000000..0d157bff --- /dev/null +++ b/wrappers/binaries.go @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +// Package wrappers is used to generate wrappers and service units and also desktop files for snap applications. +package wrappers + +import ( + "os" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +// AddSnapBinaries writes the wrapper binaries for the applications from the snap which aren't services. +func AddSnapBinaries(s *snap.Info) (err error) { + var created []string + defer func() { + if err == nil { + return + } + for _, fn := range created { + os.Remove(fn) + } + }() + + if err := os.MkdirAll(dirs.SnapBinariesDir, 0755); err != nil { + return err + } + + noCompletion := !osutil.IsWritable(dirs.CompletersDir) || !osutil.FileExists(dirs.CompletersDir) || !osutil.FileExists(dirs.CompleteSh) + for _, app := range s.Apps { + if app.IsService() { + continue + } + + wrapperPath := app.WrapperPath() + if err := os.Remove(wrapperPath); err != nil && !os.IsNotExist(err) { + return err + } + if err := os.Symlink("/usr/bin/snap", wrapperPath); err != nil { + return err + } + created = append(created, wrapperPath) + + if noCompletion || app.Completer == "" { + continue + } + // symlink the completion snippet + compPath := app.CompleterPath() + if err := os.Symlink(dirs.CompleteSh, compPath); err == nil { + created = append(created, compPath) + } else if !os.IsExist(err) { + return err + } + } + + return nil +} + +// RemoveSnapBinaries removes the wrapper binaries for the applications from the snap which aren't services from. +func RemoveSnapBinaries(s *snap.Info) error { + for _, app := range s.Apps { + os.Remove(app.WrapperPath()) + if app.Completer == "" { + continue + } + compPath := app.CompleterPath() + if target, err := os.Readlink(compPath); err == nil && target == dirs.CompleteSh { + os.Remove(compPath) + } + } + + return nil +} diff --git a/wrappers/binaries_test.go b/wrappers/binaries_test.go new file mode 100644 index 00000000..e3c853fa --- /dev/null +++ b/wrappers/binaries_test.go @@ -0,0 +1,152 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package wrappers_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/wrappers" +) + +func TestWrappers(t *testing.T) { TestingT(t) } + +type binariesTestSuite struct { + tempdir string +} + +var _ = Suite(&binariesTestSuite{}) + +func (s *binariesTestSuite) SetUpTest(c *C) { + s.tempdir = c.MkDir() + dirs.SetRootDir(s.tempdir) +} + +func (s *binariesTestSuite) TearDownTest(c *C) { + dirs.SetRootDir("") +} + +const packageHello = `name: hello-snap +version: 1.10 +summary: hello +description: Hello... +apps: + hello: + command: bin/hello + world: + command: bin/world + completer: world-completer.sh + svc1: + command: bin/hello + stop-command: bin/goodbye + post-stop-command: bin/missya + daemon: forking +` +const contentsHello = "HELLO" + +func (s *binariesTestSuite) TestAddSnapBinariesAndRemove(c *C) { + // no completers support -> no problem \o/ + c.Assert(osutil.FileExists(dirs.CompletersDir), Equals, false) + + s.testAddSnapBinariesAndRemove(c) +} + +func (s *binariesTestSuite) TestAddSnapBinariesAndRemoveWithCompleters(c *C) { + c.Assert(os.MkdirAll(dirs.CompletersDir, 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(dirs.CompleteSh), 0755), IsNil) + c.Assert(ioutil.WriteFile(dirs.CompleteSh, nil, 0644), IsNil) + // full completers support -> we get completers \o/ + + s.testAddSnapBinariesAndRemove(c) +} + +func (s *binariesTestSuite) TestAddSnapBinariesAndRemoveWithExistingCompleters(c *C) { + c.Assert(os.MkdirAll(dirs.CompletersDir, 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Dir(dirs.CompleteSh), 0755), IsNil) + c.Assert(ioutil.WriteFile(dirs.CompleteSh, nil, 0644), IsNil) + // existing completers -> they're left alone \o/ + c.Assert(ioutil.WriteFile(filepath.Join(dirs.CompletersDir, "hello-snap.world"), nil, 0644), IsNil) + + s.testAddSnapBinariesAndRemove(c) +} + +func (s *binariesTestSuite) testAddSnapBinariesAndRemove(c *C) { + info := snaptest.MockSnap(c, packageHello, contentsHello, &snap.SideInfo{Revision: snap.R(11)}) + completer := filepath.Join(dirs.CompletersDir, "hello-snap.world") + completerExisted := osutil.FileExists(completer) + + err := wrappers.AddSnapBinaries(info) + c.Assert(err, IsNil) + + bins := []string{"hello-snap.hello", "hello-snap.world"} + + for _, bin := range bins { + link := filepath.Join(dirs.SnapBinariesDir, bin) + target, err := os.Readlink(link) + c.Assert(err, IsNil, Commentf(bin)) + c.Check(target, Equals, "/usr/bin/snap", Commentf(bin)) + } + + if osutil.FileExists(dirs.CompletersDir) { + if completerExisted { + // there was a completer there before, so it should _not_ be a symlink to our complete.sh + c.Assert(osutil.IsSymlink(completer), Equals, false) + } else { + target, err := os.Readlink(completer) + c.Assert(err, IsNil) + c.Check(target, Equals, dirs.CompleteSh) + } + } + + err = wrappers.RemoveSnapBinaries(info) + c.Assert(err, IsNil) + + for _, bin := range bins { + link := filepath.Join(dirs.SnapBinariesDir, bin) + c.Check(osutil.FileExists(link), Equals, false, Commentf(bin)) + } + + // we left the existing completer alone, but removed it otherwise + c.Check(osutil.FileExists(completer), Equals, completerExisted) +} + +func (s *binariesTestSuite) TestAddSnapBinariesCleansUpOnFailure(c *C) { + link := filepath.Join(dirs.SnapBinariesDir, "hello-snap.hello") + c.Assert(osutil.FileExists(link), Equals, false) + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBinariesDir, "hello-snap.bye", "potato"), 0755), IsNil) + + info := snaptest.MockSnap(c, packageHello+` + bye: + command: bin/bye +`, contentsHello, &snap.SideInfo{Revision: snap.R(11)}) + + err := wrappers.AddSnapBinaries(info) + c.Assert(err, NotNil) + + c.Check(osutil.FileExists(link), Equals, false) +} diff --git a/wrappers/desktop.go b/wrappers/desktop.go new file mode 100644 index 00000000..93426e30 --- /dev/null +++ b/wrappers/desktop.go @@ -0,0 +1,250 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package wrappers + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +// From the freedesktop Desktop Entry Specification¹, +// +// Keys with type localestring may be postfixed by [LOCALE], where +// LOCALE is the locale type of the entry. LOCALE must be of the form +// lang_COUNTRY.ENCODING@MODIFIER, where _COUNTRY, .ENCODING, and +// @MODIFIER may be omitted. If a postfixed key occurs, the same key +// must be also present without the postfix. +// +// When reading in the desktop entry file, the value of the key is +// selected by matching the current POSIX locale for the LC_MESSAGES +// category against the LOCALE postfixes of all occurrences of the +// key, with the .ENCODING part stripped. +// +// sadly POSIX doesn't mention what values are valid for LC_MESSAGES, +// beyond mentioning² that it's implementation-defined (and can be of +// the form [language[_territory][.codeset][@modifier]]) +// +// 1. https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s04.html +// 2. http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_02 +// +// So! The following is simplistic, and based on the contents of +// PROVIDED_LOCALES in locales.config, and does not cover all of +// "locales -m" (and ignores XSetLocaleModifiers(3), which may or may +// not be related). Patches welcome, as long as it's readable. +// +// REVIEWERS: this could also be left as `(?:\[[@_.A-Za-z-]\])?=` if even +// the following is hard to read: +const localizedSuffix = `(?:\[[a-z]+(?:_[A-Z]+)?(?:\.[0-9A-Z-]+)?(?:@[a-z]+)?\])?=` + +var isValidDesktopFileLine = regexp.MustCompile(strings.Join([]string{ + // NOTE (mostly to self): as much as possible keep the + // individual regexp simple, optimize for legibility + // + // empty lines and comments + `^\s*$`, + `^\s*#`, + // headers + `^\[Desktop Entry\]$`, + `^\[Desktop Action [0-9A-Za-z-]+\]$`, + `^\[[A-Za-z0-9-]+ Shortcut Group\]$`, + // https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html + "^Type=", + "^Version=", + "^Name" + localizedSuffix, + "^GenericName" + localizedSuffix, + "^NoDisplay=", + "^Comment" + localizedSuffix, + "^Icon=", + "^Hidden=", + "^OnlyShowIn=", + "^NotShowIn=", + "^Exec=", + // Note that we do not support TryExec, it does not make sense + // in the snap context + "^Terminal=", + "^Actions=", + "^MimeType=", + "^Categories=", + "^Keywords" + localizedSuffix, + "^StartupNotify=", + "^StartupWMClass=", + // unity extension + "^X-Ayatana-Desktop-Shortcuts=", + "^TargetEnvironment=", +}, "|")).Match + +// rewriteExecLine rewrites a "Exec=" line to use the wrapper path for snap application. +func rewriteExecLine(s *snap.Info, desktopFile, line string) (string, error) { + env := fmt.Sprintf("env BAMF_DESKTOP_FILE_HINT=%s ", desktopFile) + + cmd := strings.SplitN(line, "=", 2)[1] + for _, app := range s.Apps { + wrapper := app.WrapperPath() + validCmd := filepath.Base(wrapper) + // check the prefix to allow %flag style args + // this is ok because desktop files are not run through sh + // so we don't have to worry about the arguments too much + if cmd == validCmd { + return "Exec=" + env + wrapper, nil + } else if strings.HasPrefix(cmd, validCmd+" ") { + return fmt.Sprintf("Exec=%s%s%s", env, wrapper, line[len("Exec=")+len(validCmd):]), nil + } + } + + logger.Noticef("cannot use line %q for desktop file %q (snap %s)", line, desktopFile, s.Name()) + // The Exec= line in the desktop file is invalid. Instead of failing + // hard we rewrite the Exec= line. The convention is that the desktop + // file has the same name as the application we can use this fact here. + df := filepath.Base(desktopFile) + desktopFileApp := strings.TrimSuffix(df, filepath.Ext(df)) + app, ok := s.Apps[desktopFileApp] + if ok { + newExec := fmt.Sprintf("Exec=%s%s", env, app.WrapperPath()) + logger.Noticef("rewriting desktop file %q to %q", desktopFile, newExec) + return newExec, nil + } + + return "", fmt.Errorf("invalid exec command: %q", cmd) +} + +func sanitizeDesktopFile(s *snap.Info, desktopFile string, rawcontent []byte) []byte { + var newContent bytes.Buffer + mountDir := []byte(s.MountDir()) + scanner := bufio.NewScanner(bytes.NewReader(rawcontent)) + for i := 0; scanner.Scan(); i++ { + bline := scanner.Bytes() + + if !isValidDesktopFileLine(bline) { + logger.Debugf("ignoring line %d (%q) in source of desktop file %q", i, bline, filepath.Base(desktopFile)) + continue + } + + // rewrite exec lines to an absolute path for the binary + if bytes.HasPrefix(bline, []byte("Exec=")) { + var err error + line, err := rewriteExecLine(s, desktopFile, string(bline)) + if err != nil { + // something went wrong, ignore the line + continue + } + bline = []byte(line) + } + + // do variable substitution + bline = bytes.Replace(bline, []byte("${SNAP}"), mountDir, -1) + + newContent.Grow(len(bline) + 1) + newContent.Write(bline) + newContent.WriteByte('\n') + } + + return newContent.Bytes() +} + +func updateDesktopDatabase(desktopFiles []string) error { + if len(desktopFiles) == 0 { + return nil + } + + if _, err := exec.LookPath("update-desktop-database"); err == nil { + if output, err := exec.Command("update-desktop-database", dirs.SnapDesktopFilesDir).CombinedOutput(); err != nil { + return fmt.Errorf("cannot update-desktop-database %q: %s", output, err) + } + logger.Debugf("update-desktop-database successful") + } + return nil +} + +// AddSnapDesktopFiles puts in place the desktop files for the applications from the snap. +func AddSnapDesktopFiles(s *snap.Info) (err error) { + var created []string + defer func() { + if err == nil { + return + } + + for _, fn := range created { + os.Remove(fn) + } + }() + + if err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755); err != nil { + return err + } + + baseDir := s.MountDir() + + desktopFiles, err := filepath.Glob(filepath.Join(baseDir, "meta", "gui", "*.desktop")) + if err != nil { + return fmt.Errorf("cannot get desktop files for %v: %s", baseDir, err) + } + + for _, df := range desktopFiles { + content, err := ioutil.ReadFile(df) + if err != nil { + return err + } + + installedDesktopFileName := filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s", s.Name(), filepath.Base(df))) + content = sanitizeDesktopFile(s, installedDesktopFileName, content) + if err := osutil.AtomicWriteFile(installedDesktopFileName, content, 0755, 0); err != nil { + return err + } + created = append(created, installedDesktopFileName) + } + + // updates mime info etc + if err := updateDesktopDatabase(desktopFiles); err != nil { + return err + } + + return nil +} + +// RemoveSnapDesktopFiles removes the added desktop files for the applications in the snap. +func RemoveSnapDesktopFiles(s *snap.Info) error { + glob := filepath.Join(dirs.SnapDesktopFilesDir, s.Name()+"_*.desktop") + activeDesktopFiles, err := filepath.Glob(glob) + if err != nil { + return fmt.Errorf("cannot get desktop files for %v: %s", glob, err) + } + for _, f := range activeDesktopFiles { + os.Remove(f) + } + + // updates mime info etc + if err := updateDesktopDatabase(activeDesktopFiles); err != nil { + return err + } + + return nil +} diff --git a/wrappers/desktop_test.go b/wrappers/desktop_test.go new file mode 100644 index 00000000..7f699174 --- /dev/null +++ b/wrappers/desktop_test.go @@ -0,0 +1,361 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package wrappers_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/wrappers" +) + +type desktopSuite struct { + tempdir string + + mockUpdateDesktopDatabase *testutil.MockCmd +} + +var _ = Suite(&desktopSuite{}) + +func (s *desktopSuite) SetUpTest(c *C) { + s.tempdir = c.MkDir() + dirs.SetRootDir(s.tempdir) + + s.mockUpdateDesktopDatabase = testutil.MockCommand(c, "update-desktop-database", "") +} + +func (s *desktopSuite) TearDownTest(c *C) { + s.mockUpdateDesktopDatabase.Restore() + dirs.SetRootDir("") +} + +var desktopAppYaml = ` +name: foo +version: 1.0 +` + +var mockDesktopFile = []byte(` +[Desktop Entry] +Name=foo +Icon=${SNAP}/foo.png`) +var desktopContents = "" + +func (s *desktopSuite) TestAddPackageDesktopFiles(c *C) { + expectedDesktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "foo_foobar.desktop") + c.Assert(osutil.FileExists(expectedDesktopFilePath), Equals, false) + + info := snaptest.MockSnap(c, desktopAppYaml, desktopContents, &snap.SideInfo{Revision: snap.R(11)}) + + // generate .desktop file in the package baseDir + baseDir := info.MountDir() + err := os.MkdirAll(filepath.Join(baseDir, "meta", "gui"), 0755) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(filepath.Join(baseDir, "meta", "gui", "foobar.desktop"), mockDesktopFile, 0644) + c.Assert(err, IsNil) + + err = wrappers.AddSnapDesktopFiles(info) + c.Assert(err, IsNil) + c.Assert(osutil.FileExists(expectedDesktopFilePath), Equals, true) + c.Assert(s.mockUpdateDesktopDatabase.Calls(), DeepEquals, [][]string{ + {"update-desktop-database", dirs.SnapDesktopFilesDir}, + }) +} + +func (s *desktopSuite) TestRemovePackageDesktopFiles(c *C) { + mockDesktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "foo_foobar.desktop") + + err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(mockDesktopFilePath, mockDesktopFile, 0644) + c.Assert(err, IsNil) + info, err := snap.InfoFromSnapYaml([]byte(desktopAppYaml)) + c.Assert(err, IsNil) + + err = wrappers.RemoveSnapDesktopFiles(info) + c.Assert(err, IsNil) + c.Assert(osutil.FileExists(mockDesktopFilePath), Equals, false) + c.Assert(s.mockUpdateDesktopDatabase.Calls(), DeepEquals, [][]string{ + {"update-desktop-database", dirs.SnapDesktopFilesDir}, + }) +} + +func (s *desktopSuite) TestAddPackageDesktopFilesCleanup(c *C) { + mockDesktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "foo_foobar1.desktop") + c.Assert(osutil.FileExists(mockDesktopFilePath), Equals, false) + + err := os.MkdirAll(filepath.Join(dirs.SnapDesktopFilesDir, "foo_foobar2.desktop", "potato"), 0755) + c.Assert(err, IsNil) + + info := snaptest.MockSnap(c, desktopAppYaml, desktopContents, &snap.SideInfo{Revision: snap.R(11)}) + + // generate .desktop file in the package baseDir + baseDir := info.MountDir() + err = os.MkdirAll(filepath.Join(baseDir, "meta", "gui"), 0755) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(filepath.Join(baseDir, "meta", "gui", "foobar1.desktop"), mockDesktopFile, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(baseDir, "meta", "gui", "foobar2.desktop"), mockDesktopFile, 0644) + c.Assert(err, IsNil) + + err = wrappers.AddSnapDesktopFiles(info) + c.Check(err, NotNil) + c.Check(osutil.FileExists(mockDesktopFilePath), Equals, false) + c.Check(s.mockUpdateDesktopDatabase.Calls(), HasLen, 0) +} + +// sanitize + +type sanitizeDesktopFileSuite struct{} + +var _ = Suite(&sanitizeDesktopFileSuite{}) + +func (s *sanitizeDesktopFileSuite) TestSanitizeIgnoreNotWhitelisted(c *C) { + snap := &snap.Info{SideInfo: snap.SideInfo{RealName: "foo", Revision: snap.R(12)}} + desktopContent := []byte(`[Desktop Entry] +Name=foo +UnknownKey=baz +nonsense +Icon=${SNAP}/meep + +# the empty line above is fine`) + + e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent) + c.Assert(string(e), Equals, fmt.Sprintf(`[Desktop Entry] +Name=foo +Icon=%s/foo/12/meep + +# the empty line above is fine +`, dirs.SnapMountDir)) +} + +func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersExec(c *C) { + snap, err := snap.InfoFromSnapYaml([]byte(` +name: snap +version: 1.0 +apps: + app: + command: cmd +`)) + c.Assert(err, IsNil) + desktopContent := []byte(`[Desktop Entry] +Name=foo +Exec=baz +`) + + e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent) + c.Assert(string(e), Equals, `[Desktop Entry] +Name=foo +`) +} + +func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersExecPrefix(c *C) { + snap, err := snap.InfoFromSnapYaml([]byte(` +name: snap +version: 1.0 +apps: + app: + command: cmd +`)) + c.Assert(err, IsNil) + desktopContent := []byte(`[Desktop Entry] +Name=foo +Exec=snap.app.evil.evil +`) + + e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent) + c.Assert(string(e), Equals, `[Desktop Entry] +Name=foo +`) +} + +func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersExecRewriteFromDesktop(c *C) { + snap, err := snap.InfoFromSnapYaml([]byte(` +name: snap +version: 1.0 +apps: + app: + command: cmd +`)) + c.Assert(err, IsNil) + desktopContent := []byte(`[Desktop Entry] +Name=foo +Exec=snap.app.evil.evil +`) + + e := wrappers.SanitizeDesktopFile(snap, "app.desktop", desktopContent) + c.Assert(string(e), Equals, `[Desktop Entry] +Name=foo +Exec=env BAMF_DESKTOP_FILE_HINT=app.desktop /snap/bin/snap.app +`) +} + +func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersExecOk(c *C) { + snap, err := snap.InfoFromSnapYaml([]byte(` +name: snap +version: 1.0 +apps: + app: + command: cmd +`)) + c.Assert(err, IsNil) + desktopContent := []byte(`[Desktop Entry] +Name=foo +Exec=snap.app %U +`) + + e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent) + c.Assert(string(e), Equals, fmt.Sprintf(`[Desktop Entry] +Name=foo +Exec=env BAMF_DESKTOP_FILE_HINT=foo.desktop %s/bin/snap.app %%U +`, dirs.SnapMountDir)) +} + +// we do not support TryExec (even if its a valid line), this test ensures +// we do not accidentally enable it +func (s *sanitizeDesktopFileSuite) TestSanitizeFiltersTryExecIgnored(c *C) { + snap, err := snap.InfoFromSnapYaml([]byte(` +name: snap +version: 1.0 +apps: + app: + command: cmd +`)) + c.Assert(err, IsNil) + desktopContent := []byte(`[Desktop Entry] +Name=foo +TryExec=snap.app %U +`) + + e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent) + c.Assert(string(e), Equals, `[Desktop Entry] +Name=foo +`) +} + +func (s *sanitizeDesktopFileSuite) TestSanitizeWorthWithI18n(c *C) { + snap := &snap.Info{} + desktopContent := []byte(`[Desktop Entry] +Name=foo +GenericName=bar +GenericName[de]=einsehrlangeszusammengesetzteswort +GenericName[tlh_TLH]=Qapla' +GenericName[ca@valencia]=Hola! +Invalid=key +Invalid[i18n]=key +`) + + e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent) + c.Assert(string(e), Equals, `[Desktop Entry] +Name=foo +GenericName=bar +GenericName[de]=einsehrlangeszusammengesetzteswort +GenericName[tlh_TLH]=Qapla' +GenericName[ca@valencia]=Hola! +`) +} + +func (s *sanitizeDesktopFileSuite) TestSanitizeDesktopActionsOk(c *C) { + snap := &snap.Info{} + desktopContent := []byte("[Desktop Action is-ok]\n") + + e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent) + c.Assert(string(e), Equals, string(desktopContent)) +} + +func (s *sanitizeDesktopFileSuite) TestSanitizeDesktopFileAyatana(c *C) { + snap := &snap.Info{} + + desktopContent := []byte(`[Desktop Entry] +Version=1.0 +Name=Firefox Web Browser +X-Ayatana-Desktop-Shortcuts=NewWindow;Private + +[NewWindow Shortcut Group] +Name=Open a New Window +TargetEnvironment=Unity + +[Private Shortcut Group] +Name=Private Mode +TargetEnvironment=Unity +`) + + e := wrappers.SanitizeDesktopFile(snap, "foo.desktop", desktopContent) + c.Assert(string(e), Equals, string(desktopContent)) +} + +func (s *sanitizeDesktopFileSuite) TestRewriteExecLineInvalid(c *C) { + snap := &snap.Info{} + _, err := wrappers.RewriteExecLine(snap, "foo.desktop", "Exec=invalid") + c.Assert(err, ErrorMatches, `invalid exec command: "invalid"`) +} + +func (s *sanitizeDesktopFileSuite) TestRewriteExecLineOk(c *C) { + snap, err := snap.InfoFromSnapYaml([]byte(` +name: snap +version: 1.0 +apps: + app: + command: cmd +`)) + c.Assert(err, IsNil) + + newl, err := wrappers.RewriteExecLine(snap, "foo.desktop", "Exec=snap.app") + c.Assert(err, IsNil) + c.Assert(newl, Equals, fmt.Sprintf("Exec=env BAMF_DESKTOP_FILE_HINT=foo.desktop %s/bin/snap.app", dirs.SnapMountDir)) +} + +func (s *sanitizeDesktopFileSuite) TestLangLang(c *C) { + langs := []struct { + line string + isValid bool + }{ + // langCodes + {"Name[lang]=lang-alone", true}, + {"Name[_COUNTRY]=country-alone", false}, + {"Name[.ENC-0DING]=encoding-alone", false}, + {"Name[@modifier]=modifier-alone", false}, + {"Name[lang_COUNTRY]=lang+country", true}, + {"Name[lang.ENC-0DING]=lang+encoding", true}, + {"Name[lang@modifier]=lang+modifier", true}, + // could also test all bad combos of 2, and all combos of 3... + {"Name[lang_COUNTRY.ENC-0DING@modifier]=all", true}, + // other localised entries + {"GenericName[xx]=a", true}, + {"Comment[xx]=b", true}, + {"Keywords[xx]=b", true}, + // bad ones + {"Name[foo=bar", false}, + {"Icon[xx]=bar", false}, + } + for _, t := range langs { + c.Assert(wrappers.IsValidDesktopFileLine([]byte(t.line)), Equals, t.isValid) + } +} diff --git a/wrappers/export_test.go b/wrappers/export_test.go new file mode 100644 index 00000000..118890f7 --- /dev/null +++ b/wrappers/export_test.go @@ -0,0 +1,43 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package wrappers + +import ( + "time" +) + +// some internal helper exposed for testing +var ( + // services + GenerateSnapServiceFile = generateSnapServiceFile + + // desktop + SanitizeDesktopFile = sanitizeDesktopFile + RewriteExecLine = rewriteExecLine + IsValidDesktopFileLine = isValidDesktopFileLine +) + +func MockKillWait(wait time.Duration) (restore func()) { + oldKillWait := killWait + killWait = wait + return func() { + killWait = oldKillWait + } +} diff --git a/wrappers/services.go b/wrappers/services.go new file mode 100644 index 00000000..e0efce66 --- /dev/null +++ b/wrappers/services.go @@ -0,0 +1,415 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package wrappers + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/systemd" + "github.com/snapcore/snapd/timeout" +) + +type interacter interface { + Notify(status string) +} + +// wait this time between TERM and KILL +var killWait = 5 * time.Second + +func serviceStopTimeout(app *snap.AppInfo) time.Duration { + tout := app.StopTimeout + if tout == 0 { + tout = timeout.DefaultTimeout + } + return time.Duration(tout) +} + +func generateSnapServiceFile(app *snap.AppInfo) ([]byte, error) { + if err := snap.ValidateApp(app); err != nil { + return nil, err + } + + return genServiceFile(app), nil +} + +func stopService(sysd systemd.Systemd, app *snap.AppInfo, inter interacter) error { + serviceName := app.ServiceName() + tout := serviceStopTimeout(app) + + socketErrors := []error{} + for _, socket := range app.Sockets { + if err := sysd.Stop(filepath.Base(socket.File()), tout); err != nil { + socketErrors = append(socketErrors, err) + } + } + + if err := sysd.Stop(serviceName, tout); err != nil { + if !systemd.IsTimeout(err) { + return err + } + inter.Notify(fmt.Sprintf("%s refused to stop, killing.", serviceName)) + // ignore errors for kill; nothing we'd do differently at this point + sysd.Kill(serviceName, "TERM") + time.Sleep(killWait) + sysd.Kill(serviceName, "KILL") + + } + + if len(socketErrors) > 0 { + return socketErrors[0] + } + + return nil +} + +// StartServices starts service units for the applications from the snap which are services. +func StartServices(apps []*snap.AppInfo, inter interacter) (err error) { + sysd := systemd.New(dirs.GlobalRootDir, inter) + + for _, app := range apps { + // they're *supposed* to be all services, but checking doesn't hurt + if !app.IsService() { + continue + } + + if len(app.Sockets) == 0 { + if err := sysd.Start(app.ServiceName()); err != nil { + return err + } + } + + for _, socket := range app.Sockets { + socketService := filepath.Base(socket.File()) + // enable the socket + if err := sysd.Enable(socketService); err != nil { + return err + } + + if err := sysd.Start(socketService); err != nil { + return err + } + } + + defer func(app *snap.AppInfo) { + if err == nil { + return + } + if e := stopService(sysd, app, inter); e != nil { + inter.Notify(fmt.Sprintf("While trying to stop previously started service %q: %v", app.ServiceName(), e)) + } + }(app) + } + + return nil +} + +// AddSnapServices adds service units for the applications from the snap which are services. +func AddSnapServices(s *snap.Info, inter interacter) (err error) { + sysd := systemd.New(dirs.GlobalRootDir, inter) + var written []string + var enabled []string + defer func() { + if err == nil { + return + } + for _, s := range enabled { + if e := sysd.Disable(s); e != nil { + inter.Notify(fmt.Sprintf("while trying to disable %s due to previous failure: %v", s, e)) + } + } + for _, s := range written { + if e := os.Remove(s); e != nil { + inter.Notify(fmt.Sprintf("while trying to remove %s due to previous failure: %v", s, e)) + } + } + if len(written) > 0 { + if e := sysd.DaemonReload(); e != nil { + inter.Notify(fmt.Sprintf("while trying to perform systemd daemon-reload due to previous failure: %v", e)) + } + } + }() + + for _, app := range s.Apps { + if !app.IsService() { + continue + } + // Generate service file + content, err := generateSnapServiceFile(app) + if err != nil { + return err + } + svcFilePath := app.ServiceFile() + os.MkdirAll(filepath.Dir(svcFilePath), 0755) + if err := osutil.AtomicWriteFile(svcFilePath, content, 0644, 0); err != nil { + return err + } + written = append(written, svcFilePath) + + // Generate systemd .socket files if needed + socketFiles, err := generateSnapSocketFiles(app) + if err != nil { + return err + } + for path, content := range *socketFiles { + os.MkdirAll(filepath.Dir(path), 0755) + if err := osutil.AtomicWriteFile(path, content, 0644, 0); err != nil { + return err + } + written = append(written, path) + } + + svcName := app.ServiceName() + if err := sysd.Enable(svcName); err != nil { + return err + } + enabled = append(enabled, svcName) + } + + if len(enabled) > 0 { + if err := sysd.DaemonReload(); err != nil { + return err + } + } + + return nil +} + +// StopServices stops service units for the applications from the snap which are services. +func StopServices(apps []*snap.AppInfo, inter interacter) error { + sysd := systemd.New(dirs.GlobalRootDir, inter) + + for _, app := range apps { + // Handle the case where service file doesn't exist and don't try to stop it as it will fail. + // This can happen with snap try when snap.yaml is modified on the fly and a daemon line is added. + if !app.IsService() || !osutil.FileExists(app.ServiceFile()) { + continue + } + if err := stopService(sysd, app, inter); err != nil { + return err + } + } + + return nil + +} + +// RemoveSnapServices disables and removes service units for the applications from the snap which are services. +func RemoveSnapServices(s *snap.Info, inter interacter) error { + sysd := systemd.New(dirs.GlobalRootDir, inter) + nservices := 0 + + for _, app := range s.Apps { + if !app.IsService() || !osutil.FileExists(app.ServiceFile()) { + continue + } + nservices++ + + serviceName := filepath.Base(app.ServiceFile()) + + for _, socket := range app.Sockets { + path := socket.File() + socketServiceName := filepath.Base(path) + if err := sysd.Disable(socketServiceName); err != nil { + return err + } + + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + logger.Noticef("Failed to remove socket file %q for %q: %v", path, serviceName, err) + } + } + + if err := sysd.Disable(serviceName); err != nil { + return err + } + + if err := os.Remove(app.ServiceFile()); err != nil && !os.IsNotExist(err) { + logger.Noticef("Failed to remove service file for %q: %v", serviceName, err) + } + + } + + // only reload if we actually had services + if nservices > 0 { + if err := sysd.DaemonReload(); err != nil { + return err + } + } + + return nil +} + +func genServiceFile(appInfo *snap.AppInfo) []byte { + serviceTemplate := `[Unit] +# Auto-generated, DO NOT EDIT +Description=Service for snap application {{.App.Snap.Name}}.{{.App.Name}} +Requires={{.MountUnit}} +Wants={{.PrerequisiteTarget}} +After={{.MountUnit}} {{.PrerequisiteTarget}} +X-Snappy=yes + +[Service] +ExecStart={{.App.LauncherCommand}} +SyslogIdentifier={{.App.Snap.Name}}.{{.App.Name}} +Restart={{.Restart}} +WorkingDirectory={{.App.Snap.DataDir}} +{{if .App.StopCommand}}ExecStop={{.App.LauncherStopCommand}}{{end}} +{{if .App.ReloadCommand}}ExecReload={{.App.LauncherReloadCommand}}{{end}} +{{if .App.PostStopCommand}}ExecStopPost={{.App.LauncherPostStopCommand}}{{end}} +{{if .StopTimeout}}TimeoutStopSec={{.StopTimeout.Seconds}}{{end}} +Type={{.App.Daemon}} +{{if .Remain}}RemainAfterExit={{.Remain}}{{end}} +{{if .App.BusName}}BusName={{.App.BusName}}{{end}} + +{{if not .App.Sockets}} +[Install] +WantedBy={{.ServicesTarget}} +{{end}} +` + var templateOut bytes.Buffer + t := template.Must(template.New("service-wrapper").Parse(serviceTemplate)) + + restartCond := appInfo.RestartCond.String() + if restartCond == "" { + restartCond = snap.RestartOnFailure.String() + } + + var remain string + if appInfo.Daemon == "oneshot" { + // any restart condition other than "no" is invalid for oneshot daemons + restartCond = "no" + // If StopExec is present for a oneshot service than we also need + // RemainAfterExit=yes + if appInfo.StopCommand != "" { + remain = "yes" + } + } + + wrapperData := struct { + App *snap.AppInfo + + Restart string + StopTimeout time.Duration + ServicesTarget string + PrerequisiteTarget string + MountUnit string + Remain string + + Home string + EnvVars string + }{ + App: appInfo, + + Restart: restartCond, + StopTimeout: serviceStopTimeout(appInfo), + ServicesTarget: systemd.ServicesTarget, + PrerequisiteTarget: systemd.PrerequisiteTarget, + MountUnit: filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())), + Remain: remain, + + // systemd runs as PID 1 so %h will not work. + Home: "/root", + } + + if err := t.Execute(&templateOut, wrapperData); err != nil { + // this can never happen, except we forget a variable + logger.Panicf("Unable to execute template: %v", err) + } + + return templateOut.Bytes() +} + +func genServiceSocketFile(appInfo *snap.AppInfo, socketName string) []byte { + socketTemplate := `[Unit] +# Auto-generated, DO NO EDIT +Description=Socket {{.SocketName}} for snap application {{.App.Snap.Name}}.{{.App.Name}} +Requires={{.MountUnit}} +Wants={{.PrerequisiteTarget}} +After={{.MountUnit}} {{.PrerequisiteTarget}} +X-Snappy=yes + +[Socket] +Service={{.ServiceFileName}} +FileDescriptorName={{.SocketInfo.Name}} +ListenStream={{.ListenStream}} +{{if .SocketInfo.SocketMode}}SocketMode={{.SocketInfo.SocketMode | printf "%04o"}}{{end}} + +[Install] +WantedBy={{.SocketsTarget}} +` + var templateOut bytes.Buffer + t := template.Must(template.New("socket-wrapper").Parse(socketTemplate)) + + socket := appInfo.Sockets[socketName] + listenStream := renderListenStream(socket) + wrapperData := struct { + App *snap.AppInfo + ServiceFileName string + PrerequisiteTarget string + SocketsTarget string + MountUnit string + SocketName string + SocketInfo *snap.SocketInfo + ListenStream string + }{ + App: appInfo, + ServiceFileName: filepath.Base(appInfo.ServiceFile()), + SocketsTarget: systemd.SocketsTarget, + PrerequisiteTarget: systemd.PrerequisiteTarget, + MountUnit: filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())), + SocketName: socketName, + SocketInfo: socket, + ListenStream: listenStream, + } + + if err := t.Execute(&templateOut, wrapperData); err != nil { + // this can never happen, except we forget a variable + logger.Panicf("Unable to execute template: %v", err) + } + + return templateOut.Bytes() +} + +func generateSnapSocketFiles(app *snap.AppInfo) (*map[string][]byte, error) { + if err := snap.ValidateApp(app); err != nil { + return nil, err + } + + socketFiles := make(map[string][]byte) + for name, socket := range app.Sockets { + socketFiles[socket.File()] = genServiceSocketFile(app, name) + } + return &socketFiles, nil +} + +func renderListenStream(socket *snap.SocketInfo) string { + snap := socket.App.Snap + listenStream := strings.Replace(socket.ListenStream, "$SNAP_DATA", snap.DataDir(), -1) + return strings.Replace(listenStream, "$SNAP_COMMON", snap.CommonDataDir(), -1) +} diff --git a/wrappers/services_gen_test.go b/wrappers/services_gen_test.go new file mode 100644 index 00000000..f1d49106 --- /dev/null +++ b/wrappers/services_gen_test.go @@ -0,0 +1,265 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package wrappers_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/timeout" + "github.com/snapcore/snapd/wrappers" +) + +type servicesWrapperGenSuite struct{} + +var _ = Suite(&servicesWrapperGenSuite{}) + +const expectedServiceFmt = `[Unit] +# Auto-generated, DO NOT EDIT +Description=Service for snap application snap.app +Requires=%s-snap-44.mount +Wants=network-online.target +After=%s-snap-44.mount network-online.target +X-Snappy=yes + +[Service] +ExecStart=/usr/bin/snap run snap.app +SyslogIdentifier=snap.app +Restart=%s +WorkingDirectory=/var/snap/snap/44 +ExecStop=/usr/bin/snap run --command=stop snap.app +ExecReload=/usr/bin/snap run --command=reload snap.app +ExecStopPost=/usr/bin/snap run --command=post-stop snap.app +TimeoutStopSec=10 +Type=%s + + +[Install] +WantedBy=multi-user.target + +` + +var ( + mountUnitPrefix = strings.Replace(dirs.SnapMountDir[1:], "/", "-", -1) +) + +var ( + expectedAppService = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "simple\n\n") + expectedDbusService = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "dbus\n\nBusName=foo.bar.baz") + expectedOneshotService = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "no", "oneshot\nRemainAfterExit=yes\n") +) + +var ( + expectedServiceWrapperFmt = `[Unit] +# Auto-generated, DO NOT EDIT +Description=Service for snap application xkcd-webserver.xkcd-webserver +Requires=%s-xkcd\x2dwebserver-44.mount +Wants=network-online.target +After=%s-xkcd\x2dwebserver-44.mount network-online.target +X-Snappy=yes + +[Service] +ExecStart=/usr/bin/snap run xkcd-webserver +SyslogIdentifier=xkcd-webserver.xkcd-webserver +Restart=on-failure +WorkingDirectory=/var/snap/xkcd-webserver/44 +ExecStop=/usr/bin/snap run --command=stop xkcd-webserver +ExecReload=/usr/bin/snap run --command=reload xkcd-webserver +ExecStopPost=/usr/bin/snap run --command=post-stop xkcd-webserver +TimeoutStopSec=30 +Type=%s +%s +` + expectedTypeForkingWrapper = fmt.Sprintf(expectedServiceWrapperFmt, mountUnitPrefix, mountUnitPrefix, "forking", "\n\n\n\n[Install]\nWantedBy=multi-user.target\n") +) + +func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFile(c *C) { + yamlText := ` +name: snap +version: 1.0 +apps: + app: + command: bin/start + stop-command: bin/stop + reload-command: bin/reload + post-stop-command: bin/stop --post + stop-timeout: 10s + daemon: simple +` + info, err := snap.InfoFromSnapYaml([]byte(yamlText)) + c.Assert(err, IsNil) + info.Revision = snap.R(44) + app := info.Apps["app"] + + generatedWrapper, err := wrappers.GenerateSnapServiceFile(app) + c.Assert(err, IsNil) + c.Check(string(generatedWrapper), Equals, expectedAppService) +} + +func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileRestart(c *C) { + yamlTextTemplate := ` +name: snap +apps: + app: + restart-condition: %s +` + for name, cond := range snap.RestartMap { + yamlText := fmt.Sprintf(yamlTextTemplate, cond) + + info, err := snap.InfoFromSnapYaml([]byte(yamlText)) + c.Assert(err, IsNil) + info.Revision = snap.R(44) + app := info.Apps["app"] + + generatedWrapper, err := wrappers.GenerateSnapServiceFile(app) + c.Assert(err, IsNil) + wrapperText := string(generatedWrapper) + if cond == snap.RestartNever { + c.Check(wrapperText, Matches, + `(?ms).*^Restart=no$.*`, Commentf(name)) + } else { + c.Check(wrapperText, Matches, + `(?ms).*^Restart=`+name+`$.*`, Commentf(name)) + } + } +} + +func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileTypeForking(c *C) { + service := &snap.AppInfo{ + Snap: &snap.Info{ + SuggestedName: "xkcd-webserver", + Version: "0.3.4", + SideInfo: snap.SideInfo{Revision: snap.R(44)}, + }, + Name: "xkcd-webserver", + Command: "bin/foo start", + StopCommand: "bin/foo stop", + ReloadCommand: "bin/foo reload", + PostStopCommand: "bin/foo post-stop", + StopTimeout: timeout.DefaultTimeout, + Daemon: "forking", + } + + generatedWrapper, err := wrappers.GenerateSnapServiceFile(service) + c.Assert(err, IsNil) + c.Assert(string(generatedWrapper), Equals, expectedTypeForkingWrapper) +} + +func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileIllegalChars(c *C) { + service := &snap.AppInfo{ + Snap: &snap.Info{ + SuggestedName: "xkcd-webserver", + Version: "0.3.4", + SideInfo: snap.SideInfo{Revision: snap.R(44)}, + }, + Name: "xkcd-webserver", + Command: "bin/foo start\n", + StopCommand: "bin/foo stop", + ReloadCommand: "bin/foo reload", + PostStopCommand: "bin/foo post-stop", + StopTimeout: timeout.DefaultTimeout, + Daemon: "simple", + } + + _, err := wrappers.GenerateSnapServiceFile(service) + c.Assert(err, NotNil) +} + +func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusName(c *C) { + + yamlText := ` +name: snap +version: 1.0 +apps: + app: + command: bin/start + stop-command: bin/stop + reload-command: bin/reload + post-stop-command: bin/stop --post + stop-timeout: 10s + bus-name: foo.bar.baz + daemon: dbus +` + + info, err := snap.InfoFromSnapYaml([]byte(yamlText)) + c.Assert(err, IsNil) + info.Revision = snap.R(44) + app := info.Apps["app"] + + generatedWrapper, err := wrappers.GenerateSnapServiceFile(app) + c.Assert(err, IsNil) + + c.Assert(string(generatedWrapper), Equals, expectedDbusService) +} + +func (s *servicesWrapperGenSuite) TestGenOneshotServiceFile(c *C) { + + info := snaptest.MockInfo(c, ` +name: snap +version: 1.0 +apps: + app: + command: bin/start + stop-command: bin/stop + reload-command: bin/reload + post-stop-command: bin/stop --post + stop-timeout: 10s + daemon: oneshot +`, &snap.SideInfo{Revision: snap.R(44)}) + + app := info.Apps["app"] + + generatedWrapper, err := wrappers.GenerateSnapServiceFile(app) + c.Assert(err, IsNil) + + c.Assert(string(generatedWrapper), Equals, expectedOneshotService) +} + +func (s *servicesWrapperGenSuite) TestGenerateSnapServiceWithSockets(c *C) { + service := &snap.AppInfo{ + Snap: &snap.Info{ + SuggestedName: "xkcd-webserver", + Version: "0.3.4", + SideInfo: snap.SideInfo{Revision: snap.R(44)}, + }, + Name: "xkcd-webserver", + Command: "bin/foo start", + Daemon: "simple", + Plugs: map[string]*snap.PlugInfo{"network-bind": {}}, + Sockets: map[string]*snap.SocketInfo{ + "sock1": { + Name: "sock1", + ListenStream: "$SNAP_DATA/sock1.socket", + SocketMode: 0666, + }, + }, + } + + generatedWrapper, err := wrappers.GenerateSnapServiceFile(service) + c.Assert(err, IsNil) + c.Assert(strings.Contains(string(generatedWrapper), "[Install]"), Equals, false) + c.Assert(strings.Contains(string(generatedWrapper), "WantedBy=multi-user.target"), Equals, false) +} diff --git a/wrappers/services_test.go b/wrappers/services_test.go new file mode 100644 index 00000000..a220809e --- /dev/null +++ b/wrappers/services_test.go @@ -0,0 +1,472 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package wrappers_test + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/systemd" + "github.com/snapcore/snapd/wrappers" +) + +type servicesTestSuite struct { + tempdir string + + restorer func() +} + +var _ = Suite(&servicesTestSuite{}) + +func (s *servicesTestSuite) SetUpTest(c *C) { + s.tempdir = c.MkDir() + dirs.SetRootDir(s.tempdir) + + s.restorer = systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + return []byte("ActiveState=inactive\n"), nil + }) +} + +func (s *servicesTestSuite) TearDownTest(c *C) { + dirs.SetRootDir("") + s.restorer() +} + +func (s *servicesTestSuite) TestAddSnapServicesAndRemove(c *C) { + var sysdLog [][]string + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + return []byte("ActiveState=inactive\n"), nil + }) + defer r() + + info := snaptest.MockSnap(c, packageHello, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + svcFile := filepath.Join(s.tempdir, "/etc/systemd/system/snap.hello-snap.svc1.service") + + err := wrappers.AddSnapServices(info, nil) + c.Assert(err, IsNil) + c.Check(sysdLog, DeepEquals, [][]string{ + {"--root", dirs.GlobalRootDir, "enable", filepath.Base(svcFile)}, + {"daemon-reload"}, + }) + + content, err := ioutil.ReadFile(svcFile) + c.Assert(err, IsNil) + + verbs := []string{"Start", "Stop", "StopPost"} + cmds := []string{"", " --command=stop", " --command=post-stop"} + for i := range verbs { + expected := fmt.Sprintf("Exec%s=/usr/bin/snap run%s hello-snap.svc1", verbs[i], cmds[i]) + c.Check(string(content), Matches, "(?ms).*^"+regexp.QuoteMeta(expected)) // check.v1 adds ^ and $ around the regexp provided + } + + sysdLog = nil + err = wrappers.StopServices(info.Services(), progress.Null) + c.Assert(err, IsNil) + c.Assert(sysdLog, HasLen, 2) + c.Check(sysdLog, DeepEquals, [][]string{ + {"stop", filepath.Base(svcFile)}, + {"show", "--property=ActiveState", "snap.hello-snap.svc1.service"}, + }) + + sysdLog = nil + err = wrappers.RemoveSnapServices(info, progress.Null) + c.Assert(err, IsNil) + c.Check(osutil.FileExists(svcFile), Equals, false) + c.Assert(sysdLog, HasLen, 2) + c.Check(sysdLog[0], DeepEquals, []string{"--root", dirs.GlobalRootDir, "disable", filepath.Base(svcFile)}) + c.Check(sysdLog[1], DeepEquals, []string{"daemon-reload"}) +} + +func (s *servicesTestSuite) TestRemoveSnapWithSocketsRemovesSocketsService(c *C) { + info := snaptest.MockSnap(c, packageHello+` + svc1: + daemon: simple + plugs: [network-bind] + sockets: + sock1: + listen-stream: $SNAP_DATA/sock1.socket + socket-mode: 0666 + sock2: + listen-stream: $SNAP_COMMON/sock2.socket +`, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + + err := wrappers.AddSnapServices(info, nil) + c.Assert(err, IsNil) + + err = wrappers.StopServices(info.Services(), &progress.Null) + c.Assert(err, IsNil) + + err = wrappers.RemoveSnapServices(info, &progress.Null) + c.Assert(err, IsNil) + + app := info.Apps["svc1"] + c.Assert(app.Sockets, HasLen, 2) + for _, socket := range app.Sockets { + c.Check(osutil.FileExists(socket.File()), Equals, false) + } +} + +func (s *servicesTestSuite) TestRemoveSnapPackageFallbackToKill(c *C) { + restore := wrappers.MockKillWait(200 * time.Millisecond) + defer restore() + + var sysdLog [][]string + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + // filter out the "systemctl show" that + // StopServices generates + if cmd[0] != "show" { + sysdLog = append(sysdLog, cmd) + } + return []byte("ActiveState=active\n"), nil + }) + defer r() + + info := snaptest.MockSnap(c, `name: wat +version: 42 +apps: + wat: + command: wat + stop-timeout: 250ms + daemon: forking +`, "", &snap.SideInfo{Revision: snap.R(11)}) + + err := wrappers.AddSnapServices(info, nil) + c.Assert(err, IsNil) + + sysdLog = nil + + svcFName := "snap.wat.wat.service" + + err = wrappers.StopServices(info.Services(), progress.Null) + c.Assert(err, IsNil) + + c.Check(sysdLog, DeepEquals, [][]string{ + {"stop", svcFName}, + // check kill invocations + {"kill", svcFName, "-s", "TERM"}, + {"kill", svcFName, "-s", "KILL"}, + }) +} + +func (s *servicesTestSuite) TestStopServicesWithSockets(c *C) { + var sysdLog []string + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + if cmd[0] == "stop" { + sysdLog = append(sysdLog, cmd[1]) + } + return []byte("ActiveState=inactive\n"), nil + }) + defer r() + + info := snaptest.MockSnap(c, packageHello+` + svc1: + daemon: simple + plugs: [network-bind] + sockets: + sock1: + listen-stream: $SNAP_COMMON/sock1.socket + socket-mode: 0666 + sock2: + listen-stream: $SNAP_DATA/sock2.socket +`, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + + err := wrappers.AddSnapServices(info, nil) + c.Assert(err, IsNil) + + sysdLog = nil + + err = wrappers.StopServices(info.Services(), &progress.Null) + c.Assert(err, IsNil) + + sort.Strings(sysdLog) + c.Check(sysdLog, DeepEquals, []string{ + "snap.hello-snap.svc1.service", "snap.hello-snap.svc1.sock1.socket", "snap.hello-snap.svc1.sock2.socket"}) +} + +func (s *servicesTestSuite) TestStartServices(c *C) { + var sysdLog [][]string + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + return []byte("ActiveState=inactive\n"), nil + }) + defer r() + + info := snaptest.MockSnap(c, packageHello, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + svcFile := filepath.Join(s.tempdir, "/etc/systemd/system/snap.hello-snap.svc1.service") + + err := wrappers.StartServices(info.Services(), nil) + c.Assert(err, IsNil) + + c.Assert(sysdLog, DeepEquals, [][]string{{"start", filepath.Base(svcFile)}}) +} + +func (s *servicesTestSuite) TestAddSnapMultiServicesFailCreateCleanup(c *C) { + var sysdLog [][]string + + // sanity check: there are no service files + svcFiles, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, "snap.hello-snap.*.service")) + c.Check(svcFiles, HasLen, 0) + + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + return nil, nil + }) + defer r() + + info := snaptest.MockSnap(c, packageHello+` + svc2: + daemon: potato +`, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + + err := wrappers.AddSnapServices(info, nil) + c.Assert(err, ErrorMatches, ".*potato.*") + + // the services are cleaned up + svcFiles, _ = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "snap.hello-snap.*.service")) + c.Check(svcFiles, HasLen, 0) + + // *either* the first service failed validation, and nothing + // was done, *or* the second one failed, and the first one was + // enabled before the second failed, and disabled after. + if len(sysdLog) > 0 { + // the second service failed validation + c.Check(sysdLog, DeepEquals, [][]string{ + {"--root", dirs.GlobalRootDir, "enable", "snap.hello-snap.svc1.service"}, + {"--root", dirs.GlobalRootDir, "disable", "snap.hello-snap.svc1.service"}, + {"daemon-reload"}, + }) + } +} + +func (s *servicesTestSuite) TestAddSnapMultiServicesFailEnableCleanup(c *C) { + var sysdLog [][]string + svc1Name := "snap.hello-snap.svc1.service" + svc2Name := "snap.hello-snap.svc2.service" + numEnables := 0 + + // sanity check: there are no service files + svcFiles, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, "snap.hello-snap.*.service")) + c.Check(svcFiles, HasLen, 0) + + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + sdcmd := cmd[0] + if len(cmd) >= 2 { + sdcmd = cmd[len(cmd)-2] + } + switch sdcmd { + case "enable": + numEnables++ + switch numEnables { + case 1: + if cmd[len(cmd)-1] == svc2Name { + // the services are being iterated in the "wrong" order + svc1Name, svc2Name = svc2Name, svc1Name + } + return nil, nil + case 2: + return nil, fmt.Errorf("failed") + default: + panic("expected no more than 2 enables") + } + case "disable", "daemon-reload": + return nil, nil + default: + panic("unexpected systemctl command " + sdcmd) + } + }) + defer r() + + info := snaptest.MockSnap(c, packageHello+` + svc2: + command: bin/hello + daemon: simple +`, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + + err := wrappers.AddSnapServices(info, nil) + c.Assert(err, ErrorMatches, "failed") + + // the services are cleaned up + svcFiles, _ = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "snap.hello-snap.*.service")) + c.Check(svcFiles, HasLen, 0) + c.Check(sysdLog, DeepEquals, [][]string{ + {"--root", dirs.GlobalRootDir, "enable", svc1Name}, + {"--root", dirs.GlobalRootDir, "enable", svc2Name}, // this one fails + {"--root", dirs.GlobalRootDir, "disable", svc1Name}, + {"daemon-reload"}, + }) +} + +func (s *servicesTestSuite) TestAddSnapMultiServicesStartFailOnSystemdReloadCleanup(c *C) { + // this test might be overdoing it (it's mostly covering the same ground as the previous one), but ... :-) + var sysdLog [][]string + svc1Name := "snap.hello-snap.svc1.service" + svc2Name := "snap.hello-snap.svc2.service" + + // sanity check: there are no service files + svcFiles, _ := filepath.Glob(filepath.Join(dirs.SnapServicesDir, "snap.hello-snap.*.service")) + c.Check(svcFiles, HasLen, 0) + + first := true + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + if len(cmd) < 2 { + return nil, fmt.Errorf("failed") + } + if first { + first = false + if cmd[len(cmd)-1] == svc2Name { + // the services are being iterated in the "wrong" order + svc1Name, svc2Name = svc2Name, svc1Name + } + } + return nil, nil + + }) + defer r() + + info := snaptest.MockSnap(c, packageHello+` + svc2: + command: bin/hello + daemon: simple +`, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + + err := wrappers.AddSnapServices(info, progress.Null) + c.Assert(err, ErrorMatches, "failed") + + // the services are cleaned up + svcFiles, _ = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "snap.hello-snap.*.service")) + c.Check(svcFiles, HasLen, 0) + c.Check(sysdLog, DeepEquals, [][]string{ + {"--root", dirs.GlobalRootDir, "enable", svc1Name}, + {"--root", dirs.GlobalRootDir, "enable", svc2Name}, + {"daemon-reload"}, // this one fails + {"--root", dirs.GlobalRootDir, "disable", svc1Name}, + {"--root", dirs.GlobalRootDir, "disable", svc2Name}, + {"daemon-reload"}, // so does this one :-) + }) +} + +func (s *servicesTestSuite) TestAddSnapSocketFiles(c *C) { + var sysdLog [][]string + + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + return nil, nil + }) + defer r() + + info := snaptest.MockSnap(c, packageHello+` + svc1: + daemon: simple + plugs: [network-bind] + sockets: + sock1: + listen-stream: $SNAP_COMMON/sock1.socket + socket-mode: 0666 + sock2: + listen-stream: $SNAP_DATA/sock2.socket +`, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + + sock1File := filepath.Join(s.tempdir, "/etc/systemd/system/snap.hello-snap.svc1.sock1.socket") + sock2File := filepath.Join(s.tempdir, "/etc/systemd/system/snap.hello-snap.svc1.sock2.socket") + + err := wrappers.AddSnapServices(info, nil) + c.Assert(err, IsNil) + + content1, err := ioutil.ReadFile(sock1File) + c.Assert(err, IsNil) + expected := fmt.Sprintf( + `[Socket] +Service=snap.hello-snap.svc1.service +FileDescriptorName=sock1 +ListenStream=%s +SocketMode=0666 + +`, filepath.Join(s.tempdir, "/var/snap/hello-snap/common/sock1.socket")) + c.Check(strings.Contains(string(content1), expected), Equals, true) + + content2, err := ioutil.ReadFile(sock2File) + c.Assert(err, IsNil) + + expected = fmt.Sprintf( + `[Socket] +Service=snap.hello-snap.svc1.service +FileDescriptorName=sock2 +ListenStream=%s + +`, filepath.Join(s.tempdir, "/var/snap/hello-snap/12/sock2.socket")) + c.Check(strings.Contains(string(content2), expected), Equals, true) + +} + +func (s *servicesTestSuite) TestStartSnapMultiServicesFailStartCleanup(c *C) { + var sysdLog [][]string + svc1Name := "snap.hello-snap.svc1.service" + svc2Name := "snap.hello-snap.svc2.service" + numStarts := 0 + + r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + if len(cmd) >= 2 && cmd[len(cmd)-2] == "start" { + numStarts++ + if numStarts == 2 { + name := cmd[len(cmd)-1] + if name == svc1Name { + // the services are being iterated in the "wrong" order + svc1Name, svc2Name = svc2Name, svc1Name + } + return nil, fmt.Errorf("failed") + } + } + return []byte("ActiveState=inactive\n"), nil + }) + defer r() + + info := snaptest.MockSnap(c, packageHello+` + svc2: + command: bin/hello + daemon: simple +`, contentsHello, &snap.SideInfo{Revision: snap.R(12)}) + + err := wrappers.StartServices(info.Services(), nil) + c.Assert(err, ErrorMatches, "failed") + + c.Assert(sysdLog, HasLen, 4) + c.Check(sysdLog, DeepEquals, [][]string{ + {"start", svc1Name}, + {"start", svc2Name}, // this one fails + {"stop", svc1Name}, + {"show", "--property=ActiveState", svc1Name}, + }) +} diff --git a/x11/xauth.go b/x11/xauth.go new file mode 100644 index 00000000..b6456e4d --- /dev/null +++ b/x11/xauth.go @@ -0,0 +1,151 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package x11 + +import ( + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "os" +) + +// See https://cgit.freedesktop.org/xorg/lib/libXau/tree/AuRead.c and +// https://cgit.freedesktop.org/xorg/lib/libXau/tree/include/X11/Xauth.h +// for details about the actual file format. +type xauth struct { + Family uint16 + Address []byte + Number []byte + Name []byte + Data []byte +} + +func readChunk(r io.Reader) ([]byte, error) { + // A chunk consists of a length encoded by two bytes (so max 64K) + // and additional data which is the real value of the item we're + // reading here from the file. + + b := [2]byte{} + if _, err := io.ReadFull(r, b[:]); err != nil { + return nil, err + } + + size := int(binary.BigEndian.Uint16(b[:])) + chunk := make([]byte, size) + if _, err := io.ReadFull(r, chunk); err != nil { + return nil, err + } + + return chunk, nil +} + +func (xa *xauth) readFromFile(r io.Reader) error { + b := [2]byte{} + if _, err := io.ReadFull(r, b[:]); err != nil { + return err + } + // The family field consists of two bytes + xa.Family = binary.BigEndian.Uint16(b[:]) + + var err error + + if xa.Address, err = readChunk(r); err != nil { + return err + } + + if xa.Number, err = readChunk(r); err != nil { + return err + } + + if xa.Name, err = readChunk(r); err != nil { + return err + } + + if xa.Data, err = readChunk(r); err != nil { + return err + } + + return nil +} + +// ValidateXauthority validates a given Xauthority file. The file is valid +// if it can be parsed and contains at least one cookie. +func ValidateXauthorityFile(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + return ValidateXauthority(f) +} + +// ValidateXauthority validates a given Xauthority file. The file is valid +// if it can be parsed and contains at least one cookie. +func ValidateXauthority(r io.Reader) error { + cookies := 0 + for { + xa := &xauth{} + err := xa.readFromFile(r) + if err == io.EOF { + break + } else if err != nil { + return err + } + cookies++ + } + + if cookies <= 0 { + return fmt.Errorf("Xauthority file is invalid") + } + + return nil +} + +// MockXauthority will create a fake xauthority file and place it +// on a temporary path which is returned as result. +func MockXauthority(cookies int) (string, error) { + f, err := ioutil.TempFile("", "xauth") + if err != nil { + return "", err + } + defer f.Close() + for n := 0; n < cookies; n++ { + data := []byte{ + // Family + 0x01, 0x00, + // Address + 0x00, 0x04, 0x73, 0x6e, 0x61, 0x70, + // Number + 0x00, 0x01, 0xff, + // Name + 0x00, 0x05, 0x73, 0x6e, 0x61, 0x70, 0x64, + // Data + 0x00, 0x01, 0xff, + } + m, err := f.Write(data) + if err != nil { + return "", err + } else if m != len(data) { + return "", fmt.Errorf("Could write cookie") + } + } + return f.Name(), nil +} diff --git a/x11/xauth_test.go b/x11/xauth_test.go new file mode 100644 index 00000000..e9931587 --- /dev/null +++ b/x11/xauth_test.go @@ -0,0 +1,74 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * 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 . + * + */ + +package x11_test + +import ( + "io/ioutil" + "os" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/x11" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type xauthTestSuite struct{} + +var _ = Suite(&xauthTestSuite{}) + +func (s *xauthTestSuite) TestXauthFileNotAvailable(c *C) { + err := x11.ValidateXauthorityFile("/does/not/exist") + c.Assert(err, NotNil) +} + +func (s *xauthTestSuite) TestXauthFileExistsButIsEmpty(c *C) { + xauthPath, err := x11.MockXauthority(0) + c.Assert(err, IsNil) + defer os.Remove(xauthPath) + + err = x11.ValidateXauthorityFile(xauthPath) + c.Assert(err, ErrorMatches, "Xauthority file is invalid") +} + +func (s *xauthTestSuite) TestXauthFileExistsButHasInvalidContent(c *C) { + f, err := ioutil.TempFile("", "xauth") + c.Assert(err, IsNil) + defer os.Remove(f.Name()) + + data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99} + n, err := f.Write(data) + c.Assert(err, IsNil) + c.Assert(n, Equals, len(data)) + + err = x11.ValidateXauthorityFile(f.Name()) + c.Assert(err, ErrorMatches, "unexpected EOF") +} + +func (s *xauthTestSuite) TestValidXauthFile(c *C) { + for _, n := range []int{1, 2, 4} { + path, err := x11.MockXauthority(n) + c.Assert(err, IsNil) + err = x11.ValidateXauthorityFile(path) + c.Assert(err, IsNil) + } +} -- 2.30.2