From 9074fde3375daedc7c67fa576ea8f86a6723fa63 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Thu, 24 Aug 2017 11:12:52 +0100 Subject: [PATCH] Import snapd_2.27.4.orig.tar.gz [dgit import orig snapd_2.27.4.orig.tar.gz] --- .gitignore | 62 + .travis.yml | 22 + CONTRIBUTING.md | 27 + COPYING | 674 ++ HACKING.md | 195 + README.md | 46 + arch/arch.go | 140 + arch/arch_test.go | 61 + asserts/account.go | 106 + asserts/account_key.go | 288 + asserts/account_key_test.go | 809 ++ asserts/account_test.go | 167 + asserts/asserts.go | 973 +++ asserts/asserts_test.go | 839 ++ asserts/assertstest/assertstest.go | 355 + asserts/assertstest/assertstest_test.go | 122 + asserts/crypto.go | 398 + asserts/database.go | 575 ++ asserts/database_test.go | 1012 +++ asserts/device_asserts.go | 444 ++ asserts/device_asserts_test.go | 538 ++ asserts/digest.go | 43 + asserts/digest_test.go | 65 + asserts/export_test.go | 193 + asserts/fetcher.go | 121 + asserts/fetcher_test.go | 169 + asserts/findwildcard.go | 111 + asserts/findwildcard_test.go | 139 + asserts/fsbackstore.go | 220 + asserts/fsbackstore_test.go | 251 + 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 | 184 + asserts/membackstore_test.go | 346 + asserts/memkeypairmgr.go | 59 + asserts/memkeypairmgr_test.go | 73 + asserts/privkeys_for_test.go | 54 + asserts/repair.go | 134 + asserts/repair_test.go | 177 + asserts/signtool/sign.go | 88 + asserts/signtool/sign_test.go | 179 + asserts/snap_asserts.go | 931 +++ asserts/snap_asserts_test.go | 1772 +++++ asserts/snapasserts/snapasserts.go | 144 + asserts/snapasserts/snapasserts_test.go | 317 + asserts/sysdb/staging.go | 94 + asserts/sysdb/sysdb.go | 48 + asserts/sysdb/sysdb_test.go | 147 + asserts/sysdb/testkeys.go | 30 + asserts/sysdb/trusted.go | 153 + 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/asserts.go | 91 + client/asserts_test.go | 145 + client/buy.go | 53 + client/change.go | 164 + client/change_test.go | 215 + client/client.go | 559 ++ client/client_test.go | 498 ++ client/conf.go | 50 + client/conf_test.go | 93 + client/export_test.go | 45 + client/icons.go | 66 + client/icons_test.go | 65 + client/interfaces.go | 111 + client/interfaces_test.go | 170 + client/login.go | 161 + client/login_test.go | 132 + client/packages.go | 220 + client/packages_test.go | 226 + client/snap_op.go | 255 + client/snap_op_test.go | 308 + client/snapctl.go | 57 + client/snapctl_test.go | 68 + cmd/.indent.pro | 34 + cmd/Makefile.am | 409 + cmd/autogen.sh | 52 + cmd/cmd.go | 212 + cmd/cmd_test.go | 301 + cmd/configure.ac | 213 + cmd/decode-mount-opts/decode-mount-opts.c | 38 + cmd/export_test.go | 60 + cmd/libsnap-confine-private/classic-test.c | 23 + cmd/libsnap-confine-private/classic.c | 15 + cmd/libsnap-confine-private/classic.h | 27 + .../cleanup-funcs-test.c | 41 + cmd/libsnap-confine-private/cleanup-funcs.c | 56 + cmd/libsnap-confine-private/cleanup-funcs.h | 70 + 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 | 257 + cmd/libsnap-confine-private/mount-opt.c | 324 + cmd/libsnap-confine-private/mount-opt.h | 75 + cmd/libsnap-confine-private/mountinfo-test.c | 175 + cmd/libsnap-confine-private/mountinfo.c | 253 + 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 | 192 + cmd/libsnap-confine-private/snap.c | 160 + cmd/libsnap-confine-private/snap.h | 61 + .../string-utils-test.c | 833 ++ 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 | 174 + cmd/libsnap-confine-private/utils.c | 209 + cmd/libsnap-confine-private/utils.h | 58 + cmd/snap-confine/80-snappy-assign.rules | 2 + 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 | 140 + 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 | 269 + cmd/snap-confine/mount-support-nvidia.h | 48 + cmd/snap-confine/mount-support-test.c | 100 + cmd/snap-confine/mount-support.c | 641 ++ cmd/snap-confine/mount-support.h | 44 + cmd/snap-confine/ns-support-test.c | 212 + cmd/snap-confine/ns-support.c | 477 ++ cmd/snap-confine/ns-support.h | 145 + cmd/snap-confine/quirks.c | 230 + cmd/snap-confine/quirks.h | 30 + cmd/snap-confine/seccomp-support.c | 217 + 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 | 390 + cmd/snap-confine/snap-confine.c | 275 + cmd/snap-confine/snap-confine.rst | 185 + 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 | 208 + 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/main.go | 201 + cmd/snap-exec/main_test.go | 315 + cmd/snap-repair/cmd_run.go | 43 + cmd/snap-repair/cmd_run_test.go | 33 + cmd/snap-repair/export_test.go | 26 + cmd/snap-repair/main.go | 78 + cmd/snap-repair/main_test.go | 78 + cmd/snap-seccomp/export_test.go | 41 + cmd/snap-seccomp/main.go | 701 ++ cmd/snap-seccomp/main_test.go | 607 ++ cmd/snap-update-ns/bootstrap.c | 187 + cmd/snap-update-ns/bootstrap.go | 95 + cmd/snap-update-ns/bootstrap.h | 34 + cmd/snap-update-ns/bootstrap_test.go | 108 + cmd/snap-update-ns/export_test.go | 27 + cmd/snap-update-ns/main.go | 127 + cmd/snap-update-ns/main_test.go | 32 + cmd/snap/cmd_abort.go | 60 + cmd/snap/cmd_ack.go | 75 + cmd/snap/cmd_alias.go | 114 + 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 | 315 + cmd/snap/cmd_booted.go | 50 + cmd/snap/cmd_buy.go | 137 + cmd/snap/cmd_buy_test.go | 450 ++ 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 | 85 + cmd/snap/cmd_connect_test.go | 329 + cmd/snap/cmd_create_key.go | 86 + 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 | 55 + cmd/snap/cmd_delete_key_test.go | 64 + cmd/snap/cmd_disconnect.go | 87 + cmd/snap/cmd_disconnect_test.go | 228 + cmd/snap/cmd_download.go | 133 + cmd/snap/cmd_ensure_state_soon.go | 44 + cmd/snap/cmd_ensure_state_soon_test.go | 55 + cmd/snap/cmd_export_key.go | 95 + cmd/snap/cmd_export_key_test.go | 86 + cmd/snap/cmd_find.go | 143 + cmd/snap/cmd_find_test.go | 364 + cmd/snap/cmd_first_boot.go | 49 + cmd/snap/cmd_get.go | 124 + cmd/snap/cmd_get_base_declaration.go | 51 + cmd/snap/cmd_get_base_declaration_test.go | 55 + cmd/snap/cmd_get_test.go | 105 + cmd/snap/cmd_help.go | 62 + cmd/snap/cmd_help_test.go | 81 + cmd/snap/cmd_info.go | 323 + cmd/snap/cmd_info_test.go | 62 + cmd/snap/cmd_interfaces.go | 139 + 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 | 90 + cmd/snap/cmd_list.go | 105 + cmd/snap/cmd_list_test.go | 211 + cmd/snap/cmd_login.go | 128 + 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_prefer.go | 69 + cmd/snap/cmd_prefer_test.go | 75 + cmd/snap/cmd_prepare_image.go | 73 + cmd/snap/cmd_run.go | 420 + cmd/snap/cmd_run_test.go | 568 ++ cmd/snap/cmd_set.go | 94 + cmd/snap/cmd_set_test.go | 114 + cmd/snap/cmd_shell.go | 98 + cmd/snap/cmd_sign.go | 79 + cmd/snap/cmd_sign_build.go | 115 + cmd/snap/cmd_sign_build_test.go | 134 + cmd/snap/cmd_sign_test.go | 65 + cmd/snap/cmd_snap_op.go | 974 +++ cmd/snap/cmd_snap_op_test.go | 1011 +++ cmd/snap/cmd_unalias.go | 67 + cmd/snap/cmd_unalias_test.go | 75 + 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 | 85 + cmd/snap/cmd_whoami.go | 56 + cmd/snap/complete.go | 292 + cmd/snap/error.go | 170 + cmd/snap/export_test.go | 121 + cmd/snap/gnupg2_test.go | 27 + cmd/snap/interfaces_common.go | 85 + cmd/snap/interfaces_common_test.go | 106 + cmd/snap/last.go | 85 + cmd/snap/main.go | 330 + cmd/snap/main_test.go | 279 + cmd/snap/notes.go | 163 + cmd/snap/notes_test.go | 100 + 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 | 68 + 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 + daemon/api.go | 2515 ++++++ daemon/api_mock_test.go | 124 + daemon/api_test.go | 5516 +++++++++++++ daemon/daemon.go | 430 ++ daemon/daemon_test.go | 443 ++ daemon/response.go | 262 + daemon/response_test.go | 99 + daemon/snap.go | 294 + daemon/ucrednet.go | 95 + daemon/ucrednet_test.go | 172 + data/completion/complete.sh | 116 + data/completion/etelpmoc.sh | 212 + data/completion/snap | 65 + data/failure.txt | 8 + 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 | 40 + data/systemd/snap-repair.service.in | 8 + data/systemd/snap-repair.timer | 12 + data/systemd/snapd.autoimport.service.in | 10 + data/systemd/snapd.core-fixup.service.in | 15 + 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.socket | 13 + data/systemd/snapd.system-shutdown.service.in | 15 + data/udev/rules.d/66-snapd-autoimport.rules | 3 + debian | 1 + dirs/dirs.go | 196 + dirs/dirs_test.go | 79 + docs/MOVED.md | 1 + errtracker/errtracker.go | 205 + errtracker/errtracker_test.go | 224 + errtracker/export_test.go | 91 + etc/X11/Xsession.d/65snappy | 12 + etc/profile.d/apps-bin-path.sh | 3 + gen-coverage.sh | 9 + generate-packaging-dir | 17 + get-deps.sh | 23 + httputil/export_test.go | 34 + httputil/logger.go | 121 + httputil/logger_test.go | 182 + httputil/redirect17.go | 40 + httputil/redirect18.go | 28 + httputil/retry.go | 135 + httputil/retry_test.go | 429 ++ httputil/useragent.go | 92 + httputil/useragent_test.go | 79 + httputil/withtestkeys.go | 26 + i18n/dumb/dumb.go | 24 + 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 | 215 + image/image.go | 513 ++ image/image_test.go | 836 ++ interfaces/apparmor/apparmor.go | 105 + interfaces/apparmor/apparmor_test.go | 171 + interfaces/apparmor/backend.go | 281 + interfaces/apparmor/backend_test.go | 488 ++ interfaces/apparmor/export_test.go | 49 + interfaces/apparmor/spec.go | 127 + interfaces/apparmor/spec_test.go | 101 + interfaces/apparmor/template.go | 462 ++ interfaces/apparmor/template_vars.go | 40 + interfaces/backend.go | 90 + interfaces/backends/backends.go | 48 + interfaces/builtin/account_control.go | 90 + interfaces/builtin/account_control_test.go | 115 + interfaces/builtin/all.go | 60 + interfaces/builtin/all_test.go | 309 + interfaces/builtin/alsa.go | 65 + interfaces/builtin/alsa_test.go | 101 + interfaces/builtin/autopilot.go | 76 + interfaces/builtin/autopilot_test.go | 109 + interfaces/builtin/avahi_observe.go | 133 + interfaces/builtin/avahi_observe_test.go | 100 + interfaces/builtin/bluetooth_control.go | 70 + interfaces/builtin/bluetooth_control_test.go | 115 + interfaces/builtin/bluez.go | 254 + interfaces/builtin/bluez_test.go | 171 + interfaces/builtin/bool_file.go | 164 + interfaces/builtin/bool_file_test.go | 216 + interfaces/builtin/broadcom_asic_control.go | 134 + .../builtin/broadcom_asic_control_test.go | 120 + interfaces/builtin/browser_support.go | 323 + interfaces/builtin/browser_support_test.go | 212 + interfaces/builtin/camera.go | 55 + interfaces/builtin/classic_support.go | 130 + interfaces/builtin/classic_support_test.go | 105 + interfaces/builtin/common.go | 160 + interfaces/builtin/common_test.go | 33 + interfaces/builtin/content.go | 248 + interfaces/builtin/content_test.go | 389 + interfaces/builtin/core_support.go | 107 + interfaces/builtin/core_support_test.go | 107 + interfaces/builtin/cups_control.go | 48 + interfaces/builtin/dbus.go | 439 ++ interfaces/builtin/dbus_test.go | 684 ++ interfaces/builtin/dcdbas_control.go | 64 + interfaces/builtin/dcdbas_control_test.go | 99 + interfaces/builtin/docker.go | 99 + interfaces/builtin/docker_support.go | 604 ++ interfaces/builtin/docker_support_test.go | 196 + interfaces/builtin/docker_test.go | 96 + interfaces/builtin/export_test.go | 55 + interfaces/builtin/firewall_control.go | 166 + interfaces/builtin/firewall_control_test.go | 119 + interfaces/builtin/framebuffer.go | 119 + interfaces/builtin/framebuffer_test.go | 111 + interfaces/builtin/fuse_support.go | 98 + interfaces/builtin/fuse_support_test.go | 109 + interfaces/builtin/fwupd.go | 282 + interfaces/builtin/fwupd_test.go | 172 + interfaces/builtin/gpio.go | 143 + interfaces/builtin/gpio_test.go | 152 + interfaces/builtin/greengrass_support.go | 201 + interfaces/builtin/greengrass_support_test.go | 106 + interfaces/builtin/gsettings.go | 56 + interfaces/builtin/gsettings_test.go | 117 + interfaces/builtin/hardware_observe.go | 103 + interfaces/builtin/hardware_observe_test.go | 109 + interfaces/builtin/hardware_random_control.go | 116 + .../builtin/hardware_random_control_test.go | 108 + interfaces/builtin/hardware_random_observe.go | 110 + .../builtin/hardware_random_observe_test.go | 108 + interfaces/builtin/hidraw.go | 207 + interfaces/builtin/hidraw_test.go | 266 + interfaces/builtin/home.go | 68 + interfaces/builtin/home_test.go | 100 + interfaces/builtin/i2c.go | 142 + interfaces/builtin/i2c_test.go | 210 + interfaces/builtin/iio.go | 158 + interfaces/builtin/iio_test.go | 206 + interfaces/builtin/io_ports_control.go | 131 + interfaces/builtin/io_ports_control_test.go | 139 + interfaces/builtin/joystick.go | 120 + interfaces/builtin/joystick_test.go | 113 + interfaces/builtin/kernel_module_control.go | 76 + .../builtin/kernel_module_control_test.go | 109 + interfaces/builtin/kubernetes_support.go | 99 + interfaces/builtin/kubernetes_support_test.go | 109 + interfaces/builtin/libvirt.go | 53 + interfaces/builtin/libvirt_test.go | 71 + interfaces/builtin/locale_control.go | 49 + interfaces/builtin/locale_control_test.go | 102 + interfaces/builtin/location_control.go | 263 + interfaces/builtin/location_control_test.go | 214 + interfaces/builtin/location_observe.go | 317 + interfaces/builtin/location_observe_test.go | 208 + interfaces/builtin/log_observe.go | 74 + interfaces/builtin/log_observe_test.go | 100 + interfaces/builtin/lxd.go | 98 + interfaces/builtin/lxd_support.go | 99 + interfaces/builtin/lxd_support_test.go | 108 + interfaces/builtin/lxd_test.go | 103 + interfaces/builtin/maliit.go | 187 + interfaces/builtin/maliit_test.go | 264 + interfaces/builtin/media_hub.go | 211 + interfaces/builtin/media_hub_test.go | 196 + interfaces/builtin/mir.go | 147 + interfaces/builtin/mir_test.go | 140 + interfaces/builtin/modem_manager.go | 1263 +++ interfaces/builtin/modem_manager_test.go | 247 + interfaces/builtin/mount_observe.go | 77 + interfaces/builtin/mount_observe_test.go | 100 + interfaces/builtin/mpris.go | 251 + interfaces/builtin/mpris_test.go | 346 + interfaces/builtin/netlink_audit.go | 48 + interfaces/builtin/netlink_audit_test.go | 100 + interfaces/builtin/netlink_connector.go | 51 + interfaces/builtin/netlink_connector_test.go | 100 + interfaces/builtin/network.go | 92 + interfaces/builtin/network_bind.go | 108 + interfaces/builtin/network_bind_test.go | 108 + interfaces/builtin/network_control.go | 260 + interfaces/builtin/network_control_test.go | 109 + interfaces/builtin/network_manager.go | 477 ++ interfaces/builtin/network_manager_test.go | 196 + interfaces/builtin/network_observe.go | 155 + interfaces/builtin/network_observe_test.go | 109 + interfaces/builtin/network_setup_control.go | 49 + .../builtin/network_setup_control_test.go | 99 + interfaces/builtin/network_setup_observe.go | 49 + .../builtin/network_setup_observe_test.go | 100 + interfaces/builtin/network_status.go | 161 + interfaces/builtin/network_status_test.go | 111 + interfaces/builtin/network_test.go | 109 + interfaces/builtin/ofono.go | 359 + interfaces/builtin/ofono_test.go | 201 + interfaces/builtin/online_accounts_service.go | 158 + .../builtin/online_accounts_service_test.go | 114 + interfaces/builtin/opengl.go | 76 + interfaces/builtin/openvswitch.go | 45 + interfaces/builtin/openvswitch_support.go | 44 + .../builtin/openvswitch_support_test.go | 94 + interfaces/builtin/openvswitch_test.go | 99 + interfaces/builtin/optical_drive.go | 47 + .../builtin/password_manager_service.go | 104 + .../builtin/password_manager_service_test.go | 101 + interfaces/builtin/physical_memory_control.go | 118 + .../builtin/physical_memory_control_test.go | 124 + interfaces/builtin/physical_memory_observe.go | 114 + .../builtin/physical_memory_observe_test.go | 121 + interfaces/builtin/ppp.go | 101 + interfaces/builtin/ppp_test.go | 107 + interfaces/builtin/process_control.go | 70 + interfaces/builtin/process_control_test.go | 108 + interfaces/builtin/pulseaudio.go | 177 + interfaces/builtin/pulseaudio_test.go | 116 + interfaces/builtin/raw_usb.go | 58 + interfaces/builtin/raw_usb_test.go | 100 + interfaces/builtin/removable_media.go | 50 + interfaces/builtin/removable_media_test.go | 99 + interfaces/builtin/screen_inhibit_control.go | 84 + .../builtin/screen_inhibit_control_test.go | 100 + interfaces/builtin/serial_port.go | 213 + interfaces/builtin/serial_port_test.go | 344 + interfaces/builtin/shutdown.go | 77 + interfaces/builtin/shutdown_test.go | 98 + interfaces/builtin/snapd_control.go | 56 + interfaces/builtin/snapd_control_test.go | 99 + .../builtin/storage_framework_service.go | 176 + .../builtin/storage_framework_service_test.go | 108 + interfaces/builtin/system_observe.go | 111 + interfaces/builtin/system_observe_test.go | 109 + interfaces/builtin/system_trace.go | 75 + interfaces/builtin/system_trace_test.go | 100 + interfaces/builtin/thumbnailer_service.go | 160 + .../builtin/thumbnailer_service_test.go | 124 + interfaces/builtin/time_control.go | 169 + interfaces/builtin/time_control_test.go | 111 + interfaces/builtin/timeserver_control.go | 93 + interfaces/builtin/timeserver_control_test.go | 100 + interfaces/builtin/timezone_control.go | 95 + interfaces/builtin/timezone_control_test.go | 100 + interfaces/builtin/tpm.go | 48 + interfaces/builtin/tpm_test.go | 100 + interfaces/builtin/ubuntu_download_manager.go | 260 + .../builtin/ubuntu_download_manager_test.go | 100 + interfaces/builtin/udisks2.go | 424 + interfaces/builtin/udisks2_test.go | 261 + interfaces/builtin/uhid.go | 107 + interfaces/builtin/uhid_test.go | 105 + interfaces/builtin/unity7.go | 610 ++ interfaces/builtin/unity7_test.go | 110 + interfaces/builtin/unity8.go | 152 + interfaces/builtin/unity8_calendar.go | 157 + interfaces/builtin/unity8_calendar_test.go | 225 + interfaces/builtin/unity8_contacts.go | 194 + interfaces/builtin/unity8_contacts_test.go | 228 + interfaces/builtin/unity8_pim_common.go | 186 + interfaces/builtin/unity8_test.go | 125 + interfaces/builtin/upower_observe.go | 291 + interfaces/builtin/upower_observe_test.go | 254 + interfaces/builtin/utils.go | 93 + interfaces/builtin/x11.go | 75 + interfaces/builtin/x11_test.go | 111 + interfaces/core.go | 221 + interfaces/core_test.go | 179 + interfaces/dbus/backend.go | 135 + interfaces/dbus/backend_test.go | 277 + interfaces/dbus/dbus.go | 52 + interfaces/dbus/dbus_test.go | 42 + interfaces/dbus/export_test.go | 35 + interfaces/dbus/spec.go | 131 + interfaces/dbus/spec_test.go | 105 + interfaces/dbus/template.go | 29 + interfaces/ifacetest/backend.go | 73 + interfaces/ifacetest/backendtest.go | 197 + interfaces/ifacetest/ifacetest_test.go | 30 + interfaces/ifacetest/spec.go | 80 + interfaces/ifacetest/spec_test.go | 93 + interfaces/ifacetest/testiface.go | 402 + interfaces/ifacetest/testiface_test.go | 187 + interfaces/json.go | 80 + interfaces/json_test.go | 107 + interfaces/kmod/backend.go | 128 + interfaces/kmod/backend_test.go | 135 + interfaces/kmod/export_test.go | 24 + interfaces/kmod/kmod.go | 45 + interfaces/kmod/kmod_test.go | 57 + interfaces/kmod/spec.go | 103 + interfaces/kmod/spec_test.go | 129 + interfaces/mount/backend.go | 118 + interfaces/mount/backend_test.go | 177 + interfaces/mount/change.go | 145 + interfaces/mount/change_test.go | 176 + interfaces/mount/entry.go | 199 + interfaces/mount/entry_test.go | 164 + 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 | 147 + interfaces/mount/profile.go | 105 + interfaces/mount/profile_test.go | 133 + interfaces/mount/sorting.go | 42 + interfaces/mount/sorting_test.go | 48 + interfaces/mount/spec.go | 92 + interfaces/mount/spec_test.go | 93 + interfaces/naming.go | 34 + interfaces/naming_test.go | 34 + interfaces/policy/basedeclaration.go | 199 + interfaces/policy/basedeclaration_test.go | 787 ++ 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 | 954 +++ interfaces/repo_test.go | 1847 +++++ interfaces/seccomp/backend.go | 187 + interfaces/seccomp/backend_test.go | 341 + interfaces/seccomp/export_test.go | 38 + interfaces/seccomp/seccomp_test.go | 30 + interfaces/seccomp/spec.go | 136 + interfaces/seccomp/spec_test.go | 105 + interfaces/seccomp/template.go | 566 ++ interfaces/sorting.go | 75 + interfaces/sorting_test.go | 94 + interfaces/systemd/backend.go | 162 + interfaces/systemd/backend_test.go | 150 + interfaces/systemd/service.go | 58 + interfaces/systemd/service_test.go | 45 + interfaces/systemd/spec.go | 106 + interfaces/systemd/spec_test.go | 55 + interfaces/systemd/systemd_test.go | 30 + interfaces/udev/backend.go | 139 + interfaces/udev/backend_test.go | 390 + interfaces/udev/spec.go | 95 + interfaces/udev/spec_test.go | 91 + interfaces/udev/udev.go | 41 + interfaces/udev/udev_test.go | 87 + logger/export_test.go | 27 + logger/logger.go | 125 + logger/logger_test.go | 114 + 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 | 102 + osutil/env_test.go | 134 + osutil/exec.go | 283 + osutil/exec_test.go | 226 + osutil/exitcode.go | 37 + osutil/exitcode_test.go | 56 + osutil/export_test.go | 87 + osutil/flock.go | 73 + osutil/flock_test.go | 145 + osutil/io.go | 103 + osutil/io_test.go | 176 + osutil/mkdirallchown.go | 72 + osutil/mount.go | 58 + osutil/mount_test.go | 87 + osutil/osutil_test.go | 10 + osutil/outputerr.go | 39 + osutil/outputerr_test.go | 54 + osutil/stat.go | 78 + osutil/stat_test.go | 102 + 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 | 137 + overlord/assertstate/assertstate.go | 362 + overlord/assertstate/assertstate_test.go | 1144 +++ overlord/assertstate/export_test.go | 25 + overlord/assertstate/helpers.go | 127 + overlord/auth/auth.go | 464 ++ overlord/auth/auth_test.go | 667 ++ 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 | 228 + overlord/configstate/config/helpers_test.go | 138 + overlord/configstate/config/transaction.go | 254 + .../configstate/config/transaction_test.go | 284 + overlord/configstate/configmgr.go | 44 + overlord/configstate/configstate.go | 74 + overlord/configstate/configstate_test.go | 131 + overlord/configstate/export_test.go | 22 + overlord/configstate/handler_test.go | 207 + overlord/configstate/hooks.go | 124 + overlord/devicestate/crypto.go | 80 + overlord/devicestate/devicemgr.go | 398 + overlord/devicestate/devicestate.go | 194 + overlord/devicestate/devicestate_test.go | 1559 ++++ overlord/devicestate/export_test.go | 118 + overlord/devicestate/firstboot.go | 291 + overlord/devicestate/firstboot_test.go | 930 +++ overlord/devicestate/handlers.go | 482 ++ overlord/export_test.go | 67 + overlord/hookstate/context.go | 242 + overlord/hookstate/context_test.go | 153 + overlord/hookstate/ctlcmd/ctlcmd.go | 115 + overlord/hookstate/ctlcmd/ctlcmd_test.go | 76 + overlord/hookstate/ctlcmd/export_test.go | 69 + overlord/hookstate/ctlcmd/get.go | 305 + overlord/hookstate/ctlcmd/get_test.go | 319 + overlord/hookstate/ctlcmd/set.go | 210 + overlord/hookstate/ctlcmd/set_test.go | 292 + overlord/hookstate/export_test.go | 46 + overlord/hookstate/hookmgr.go | 363 + overlord/hookstate/hooks.go | 83 + overlord/hookstate/hookstate.go | 39 + overlord/hookstate/hookstate_test.go | 817 ++ 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 | 553 ++ overlord/ifacestate/helpers.go | 448 ++ overlord/ifacestate/hooks.go | 74 + overlord/ifacestate/ifacemgr.go | 109 + overlord/ifacestate/ifacestate.go | 209 + overlord/ifacestate/ifacestate_test.go | 2082 +++++ overlord/ifacestate/implicit.go | 57 + overlord/ifacestate/implicit_test.go | 71 + overlord/managers_test.go | 1821 +++++ overlord/overlord.go | 345 + overlord/overlord_test.go | 669 ++ overlord/patch/export_test.go | 49 + 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/snapstate/aliasesv2.go | 696 ++ overlord/snapstate/aliasesv2_test.go | 1201 +++ overlord/snapstate/backend.go | 77 + overlord/snapstate/backend/aliases.go | 95 + overlord/snapstate/backend/aliases_test.go | 205 + overlord/snapstate/backend/backend.go | 51 + overlord/snapstate/backend/backend_test.go | 89 + overlord/snapstate/backend/copydata.go | 85 + overlord/snapstate/backend/copydata_test.go | 447 ++ overlord/snapstate/backend/export_test.go | 25 + overlord/snapstate/backend/link.go | 163 + overlord/snapstate/backend/link_test.go | 220 + 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 | 246 + overlord/snapstate/backend/snapdata.go | 210 + overlord/snapstate/backend/utils.go | 30 + overlord/snapstate/backend_test.go | 586 ++ overlord/snapstate/booted.go | 169 + overlord/snapstate/booted_test.go | 277 + overlord/snapstate/check_snap.go | 312 + overlord/snapstate/check_snap_test.go | 614 ++ overlord/snapstate/export_test.go | 118 + overlord/snapstate/flags.go | 68 + overlord/snapstate/handlers.go | 1467 ++++ 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 | 389 + 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/snapmgr.go | 696 ++ overlord/snapstate/snapstate.go | 1593 ++++ overlord/snapstate/snapstate_test.go | 6808 +++++++++++++++++ overlord/snapstate/storehelpers.go | 80 + overlord/state/change.go | 586 ++ overlord/state/change_test.go | 655 ++ 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 | 430 ++ overlord/state/taskrunner_test.go | 719 ++ overlord/stateengine.go | 138 + overlord/stateengine_test.go | 123 + packaging/fedora-25 | 1 + packaging/fedora/snap-mgmt.sh | 130 + packaging/fedora/snapd.spec | 1047 +++ 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 | 88 + packaging/opensuse-42.2/snapd.spec | 284 + packaging/ubuntu-14.04/changelog | 3406 +++++++++ packaging/ubuntu-14.04/compat | 1 + packaging/ubuntu-14.04/control | 120 + packaging/ubuntu-14.04/copyright | 22 + packaging/ubuntu-14.04/gbp.conf | 4 + .../golang-github-snapcore-snapd-dev.install | 1 + packaging/ubuntu-14.04/rules | 182 + packaging/ubuntu-14.04/snap.mount.service | 15 + .../ubuntu-14.04/snapd.autoimport.service | 10 + packaging/ubuntu-14.04/snapd.autoimport.udev | 3 + packaging/ubuntu-14.04/snapd.dirs | 10 + packaging/ubuntu-14.04/snapd.install | 33 + packaging/ubuntu-14.04/snapd.maintscript | 4 + packaging/ubuntu-14.04/snapd.manpages | 1 + packaging/ubuntu-14.04/snapd.postinst | 31 + packaging/ubuntu-14.04/snapd.postrm | 94 + packaging/ubuntu-14.04/snapd.prerm | 8 + packaging/ubuntu-14.04/snapd.refresh.service | 11 + packaging/ubuntu-14.04/snapd.refresh.timer | 14 + packaging/ubuntu-14.04/snapd.service | 12 + packaging/ubuntu-14.04/snapd.socket | 13 + .../snapd.system-shutdown.service | 15 + packaging/ubuntu-14.04/source/format | 1 + packaging/ubuntu-14.04/tests/README.md | 10 + packaging/ubuntu-14.04/tests/control | 11 + packaging/ubuntu-14.04/tests/integrationtests | 41 + packaging/ubuntu-14.04/tests/testconfig.json | 3 + packaging/ubuntu-14.04/ubuntu-snappy-cli.dirs | 2 + packaging/ubuntu-16.04/changelog | 3430 +++++++++ packaging/ubuntu-16.04/compat | 1 + packaging/ubuntu-16.04/control | 114 + packaging/ubuntu-16.04/copyright | 22 + .../golang-github-snapcore-snapd-dev.install | 1 + packaging/ubuntu-16.04/rules | 243 + .../ubuntu-16.04/snap-confine.maintscript | 1 + packaging/ubuntu-16.04/snapd.autoimport.udev | 3 + packaging/ubuntu-16.04/snapd.dirs | 10 + packaging/ubuntu-16.04/snapd.install | 33 + 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 | 87 + 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 | 291 + partition/ubootenv/env_test.go | 283 + partition/ubootenv/export_test.go | 24 + partition/utils.go | 52 + partition/utils_test.go | 51 + po/de.po | 1335 ++++ po/es.po | 469 ++ po/gl.po | 523 ++ po/ug.po | 452 ++ progress/progress.go | 199 + progress/progress_test.go | 150 + release/export_linux_test.go | 24 + release/export_test.go | 41 + release/release.go | 177 + release/release_test.go | 127 + release/uname_linux.go | 52 + release/uname_linux_test.go | 52 + run-checks | 215 + snap/broken.go | 99 + snap/broken_test.go | 115 + snap/container.go | 94 + snap/container_test.go | 54 + snap/errors.go | 50 + snap/export_test.go | 34 + snap/gadget.go | 132 + snap/gadget_test.go | 221 + snap/hooktypes.go | 62 + snap/implicit.go | 83 + snap/implicit_test.go | 28 + snap/info.go | 645 ++ snap/info_snap_yaml.go | 473 ++ snap/info_snap_yaml_test.go | 1430 ++++ snap/info_test.go | 712 ++ 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 | 133 + snap/snapenv/snapenv_test.go | 154 + snap/snaptest/build.go | 270 + snap/snaptest/build_test.go | 283 + snap/snaptest/export_test.go | 26 + snap/snaptest/snaptest.go | 144 + snap/snaptest/snaptest_test.go | 107 + snap/squashfs/squashfs.go | 176 + snap/squashfs/squashfs_test.go | 186 + snap/types.go | 117 + snap/types_test.go | 204 + snap/validate.go | 172 + snap/validate_test.go | 313 + spread.yaml | 522 ++ store/auth.go | 314 + store/auth_test.go | 431 ++ store/details.go | 93 + store/errors.go | 121 + store/export_test.go | 35 + store/store.go | 1830 +++++ store/store_test.go | 4665 +++++++++++ store/userinfo.go | 86 + store/userinfo_test.go | 112 + 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 | 53 + systemd/sdnotify.go | 59 + systemd/sdnotify_test.go | 87 + systemd/systemd.go | 451 ++ systemd/systemd_test.go | 474 ++ 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 | 4 + tests/completion/dirs.sh | 1 + tests/completion/dirs.vars | 8 + tests/completion/files.complete | 3 + tests/completion/files.sh | 1 + tests/completion/files.vars | 7 + tests/completion/func.complete | 7 + tests/completion/func.sh | 0 tests/completion/func.vars | 7 + tests/completion/funky.complete | 3 + tests/completion/funky.sh | 0 tests/completion/funky.vars | 7 + tests/completion/funkyfunc.complete | 11 + tests/completion/funkyfunc.sh | 0 tests/completion/funkyfunc.vars | 7 + tests/completion/hosts.complete | 3 + tests/completion/hosts.sh | 1 + tests/completion/hosts.vars | 7 + tests/completion/hosts_n_dirs.complete | 3 + tests/completion/hosts_n_dirs.sh | 2 + tests/completion/hosts_n_dirs.vars | 7 + tests/completion/indirect/task.exp | 18 + tests/completion/indirect/task.yaml | 22 + tests/completion/lib.exp0 | 70 + tests/completion/plain.complete | 3 + tests/completion/plain.sh | 0 tests/completion/plain.vars | 7 + tests/completion/plain_plusdirs.complete | 3 + 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/twisted.complete | 3 + 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/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 | 28 + tests/lib/changes.sh | 8 + tests/lib/dirs.sh | 13 + tests/lib/external/prepare-ssh.sh | 15 + tests/lib/fakedevicesvc/main.go | 135 + tests/lib/fakestore/cmd/fakestore/main.go | 98 + tests/lib/fakestore/refresh/refresh.go | 243 + tests/lib/fakestore/store/store.go | 575 ++ tests/lib/fakestore/store/store_test.go | 392 + tests/lib/mkpinentry.sh | 6 + 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 | 323 + tests/lib/prepare-project.sh | 274 + tests/lib/prepare.sh | 470 ++ tests/lib/quiet.sh | 30 + tests/lib/ramdisk.sh | 7 + tests/lib/reset.sh | 111 + tests/lib/snapbuild/main.go | 42 + tests/lib/snaps.sh | 45 + .../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 | 3 + .../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 | 2 + .../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/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/basic/meta/snap.yaml | 4 + .../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 + tests/lib/snaps/complexion/bin/complexion | 7 + .../complexion/complexion.bash-completer | 30 + tests/lib/snaps/complexion/meta/snap.yaml | 7 + .../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 + 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 | 75 + tests/lib/snaps/snapctl-hooks/meta/icon.png | Bin 0 -> 3371 bytes tests/lib/snaps/snapctl-hooks/meta/snap.yaml | 2 + .../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 + .../bin/classic-confinement | 4 + .../meta/icon.png | Bin 0 -> 3371 bytes .../meta/snap.yaml | 6 + .../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 | 12 + .../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-devmode/meta/snap.yaml | 8 + .../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 + .../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 + .../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-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/meta/snap.yaml | 8 + tests/lib/snaps/test-snapd-sh/meta/snap.yaml | 6 + .../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 + .../snapcraft.yaml | 14 + .../meta/hooks/configure | 2 + .../test-snapd-with-configure/meta/snap.yaml | 4 + .../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 | 68 + tests/lib/systemd-escape/main.go | 52 + tests/lib/systemd.sh | 20 + 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 | 53 + tests/main/change-errors/task.yaml | 10 + tests/main/chattr/task.yaml | 19 + tests/main/chattr/toggle.go | 51 + tests/main/classic-confinement/task.yaml | 47 + .../main/classic-custom-device-reg/task.yaml | 75 + tests/main/classic-firstboot/task.yaml | 72 + .../task.yaml | 38 + .../task.yaml | 52 + .../classic-ubuntu-core-transition/task.yaml | 100 + tests/main/cmdline/task.yaml | 10 + tests/main/completion/abort.exp | 7 + tests/main/completion/ack.exp | 8 + 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 | 10 + tests/main/completion/install.exp | 21 + 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 | 28 + 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 | 29 + .../test-snapd-hello-classic/Makefile | 77 + .../test-snapd-hello-classic.c | 7 + 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 | 30 + tests/main/debs-have-built-using/task.yaml | 10 + tests/main/debug-confinement/task.yaml | 12 + .../main/dirs-not-shared-with-host/task.yaml | 30 + tests/main/econnreset/task.yaml | 41 + .../main/enable-disable-units-gpio/task.yaml | 87 + tests/main/enable-disable/task.yaml | 43 + tests/main/failover/task.yaml | 109 + tests/main/find-private/successful_login.exp | 12 + tests/main/find-private/task.yaml | 48 + tests/main/help/task.yaml | 13 + tests/main/i18n/task.yaml | 16 + tests/main/install-errors/task.yaml | 63 + tests/main/install-hook/task.yaml | 58 + tests/main/install-remove-multi/task.yaml | 12 + tests/main/install-sideload/task.yaml | 77 + tests/main/install-store-laaaarge/task.yaml | 16 + tests/main/install-store/task.yaml | 38 + .../main/interfaces-account-control/task.yaml | 30 + tests/main/interfaces-alsa/task.yaml | 97 + tests/main/interfaces-avahi-observe/task.yaml | 52 + .../interfaces-bluetooth-control/task.yaml | 60 + tests/main/interfaces-bluez/task.yaml | 19 + tests/main/interfaces-cli/task.yaml | 28 + .../task.yaml | 41 + tests/main/interfaces-content/task.yaml | 48 + tests/main/interfaces-cups-control/task.yaml | 78 + tests/main/interfaces-dbus/task.yaml | 91 + .../interfaces-firewall-control/task.yaml | 87 + tests/main/interfaces-fuse_support/task.yaml | 61 + .../interfaces-hardware-observe/task.yaml | 49 + tests/main/interfaces-home/task.yaml | 155 + tests/main/interfaces-hooks/task.yaml | 21 + tests/main/interfaces-iio/task.yaml | 48 + .../task.yaml | 129 + tests/main/interfaces-libvirt/task.yaml | 82 + .../main/interfaces-locale-control/task.yaml | 104 + tests/main/interfaces-log-observe/task.yaml | 68 + tests/main/interfaces-mount-observe/task.yaml | 67 + tests/main/interfaces-network-bind/task.yaml | 77 + .../task.yaml | 35 + .../main/interfaces-network-control/task.yaml | 157 + .../main/interfaces-network-observe/task.yaml | 66 + tests/main/interfaces-network/task.yaml | 77 + tests/main/interfaces-openvswitch/task.yaml | 118 + .../task.yaml | 54 + .../main/interfaces-process-control/task.yaml | 61 + .../task.yaml | 49 + 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 + .../main/interfaces-upower-observe/task.yaml | 62 + tests/main/known-remote/task.yaml | 8 + tests/main/known/task.yaml | 15 + tests/main/listing/task.yaml | 38 + 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/manpages/task.yaml | 17 + tests/main/media-sharing/task.yaml | 21 + tests/main/op-install-failed-undone/task.yaml | 53 + tests/main/op-remove-retry/task.yaml | 47 + tests/main/op-remove/task.yaml | 32 + tests/main/postrm-purge/task.yaml | 28 + tests/main/prefer/task.yaml | 34 + tests/main/prepare-image-grub/task.yaml | 78 + tests/main/prepare-image-uboot/task.yaml | 69 + tests/main/refresh-all-undo/task.yaml | 72 + tests/main/refresh-all/task.yaml | 54 + .../task.yaml | 37 + .../task.yaml | 41 + 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 | 45 + tests/main/refresh/task.yaml | 109 + .../regression-home-snap-root-owned/task.yaml | 34 + tests/main/remove-errors/task.yaml | 15 + tests/main/revert-devmode/task.yaml | 81 + tests/main/revert-sideload/task.yaml | 20 + tests/main/revert/task.yaml | 94 + tests/main/searching/task.yaml | 41 + tests/main/security-apparmor/task.yaml | 22 + tests/main/security-device-cgroups/task.yaml | 69 + tests/main/security-devpts/pts.exp | 38 + tests/main/security-devpts/task.yaml | 13 + tests/main/security-private-tmp/task.yaml | 48 + .../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 | 34 + .../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 | 27 + tests/main/snap-confine-privs/task.yaml | 63 + tests/main/snap-confine-privs/uids-and-gids.c | 40 + 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 | 32 + tests/main/snap-env/task.yaml | 53 + tests/main/snap-get/task.yaml | 52 + tests/main/snap-info/check.py | 141 + tests/main/snap-info/task.yaml | 28 + .../main/snap-multi-service-failing/task.yaml | 10 + tests/main/snap-on-non-shared-root/task.yaml | 34 + tests/main/snap-remove-not-mounted/task.yaml | 15 + tests/main/snap-repair/task.yaml | 20 + tests/main/snap-run-alias/task.yaml | 39 + tests/main/snap-run-hook/task.yaml | 48 + 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-seccomp/task.yaml | 127 + tests/main/snap-service/task.yaml | 18 + 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-update-ns/task.yaml | 77 + tests/main/snapctl-from-snap/task.yaml | 68 + tests/main/snapctl/task.yaml | 26 + tests/main/snapd-notify/task.yaml | 17 + tests/main/snapd-reexec/task.yaml | 93 + 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 | 92 + 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-uboot/task.yaml | 12 + tests/main/ubuntu-core-upgrade/task.yaml | 84 + .../main/ubuntu-core-writablepaths/task.yaml | 38 + 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/manual-tests.md | 236 + tests/nested/core-revert/task.yaml | 77 + tests/nested/extra-snaps-assertions/task.yaml | 67 + tests/nested/image-build/task.yaml | 25 + tests/nightly/docker/task.yaml | 43 + tests/nightly/unity/task.yaml | 31 + tests/regression/lp-1595444/task.yaml | 28 + tests/regression/lp-1597839/task.yaml | 13 + tests/regression/lp-1597842/task.yaml | 23 + tests/regression/lp-1599891/task.yaml | 10 + 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 | 28 + 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 | 22 + tests/unit/c-unit-tests/task.yaml | 38 + tests/unit/gccgo/task.yaml | 19 + tests/unit/go/task.yaml | 31 + tests/upgrade/basic/task.yaml | 72 + tests/util/benchmark.sh | 22 + testutil/base.go | 51 + testutil/checkers.go | 152 + testutil/checkers_test.go | 258 + 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 | 251 + timeutil/schedule_test.go | 262 + update-pot | 39 + vendor/vendor.json | 138 + wrappers/binaries.go | 59 + wrappers/binaries_test.go | 83 + wrappers/desktop.go | 224 + wrappers/desktop_test.go | 315 + wrappers/export_test.go | 43 + wrappers/services.go | 272 + wrappers/services_gen_test.go | 238 + wrappers/services_test.go | 202 + x11/xauth.go | 151 + x11/xauth_test.go | 74 + 1523 files changed, 229377 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 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/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/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/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/80-snappy-assign.rules 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/main.go create mode 100644 cmd/snap-exec/main_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/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-seccomp/export_test.go create mode 100644 cmd/snap-seccomp/main.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_test.go create mode 100644 cmd/snap-update-ns/export_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/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_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_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_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_run.go create mode 100644 cmd/snap/cmd_run_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_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 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/completion/complete.sh create mode 100755 data/completion/etelpmoc.sh create mode 100644 data/completion/snap create mode 100644 data/failure.txt 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/snap-repair.service.in create mode 100644 data/systemd/snap-repair.timer 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.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 100644 etc/X11/Xsession.d/65snappy create mode 100644 etc/profile.d/apps-bin-path.sh 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/useragent.go create mode 100644 httputil/useragent_test.go create mode 100644 httputil/withtestkeys.go create mode 100644 i18n/dumb/dumb.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/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_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/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/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/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/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/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/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/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/x11.go create mode 100644 interfaces/builtin/x11_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/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/change.go create mode 100644 interfaces/mount/change_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/sorting.go create mode 100644 interfaces/mount/sorting_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 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/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/set.go create mode 100644 overlord/hookstate/ctlcmd/set_test.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/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/snapstate/aliasesv2.go create mode 100644 overlord/snapstate/aliasesv2_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/check_snap.go create mode 100644 overlord/snapstate/check_snap_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/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 120000 packaging/fedora-25 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 100644 packaging/ubuntu-14.04/compat create mode 100644 packaging/ubuntu-14.04/control create mode 100644 packaging/ubuntu-14.04/copyright create mode 100644 packaging/ubuntu-14.04/gbp.conf create mode 100644 packaging/ubuntu-14.04/golang-github-snapcore-snapd-dev.install create mode 100755 packaging/ubuntu-14.04/rules create mode 100644 packaging/ubuntu-14.04/snap.mount.service create mode 100644 packaging/ubuntu-14.04/snapd.autoimport.service create mode 100644 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 100644 packaging/ubuntu-14.04/snapd.maintscript create mode 100644 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 100644 packaging/ubuntu-14.04/snapd.refresh.service create mode 100644 packaging/ubuntu-14.04/snapd.refresh.timer create mode 100644 packaging/ubuntu-14.04/snapd.service create mode 100644 packaging/ubuntu-14.04/snapd.socket create mode 100644 packaging/ubuntu-14.04/snapd.system-shutdown.service create mode 100644 packaging/ubuntu-14.04/source/format create mode 100644 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 100644 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/de.po create mode 100644 po/es.po create mode 100644 po/gl.po create mode 100644 po/ug.po create mode 100644 progress/progress.go create mode 100644 progress/progress_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/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/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/build.go create mode 100644 snap/snaptest/build_test.go create mode 100644 snap/snaptest/export_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 spread.yaml create mode 100644 store/auth.go create mode 100644 store/auth_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/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/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/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 100755 tests/lib/changes.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/main.go create mode 100644 tests/lib/fakestore/refresh/refresh.go create mode 100644 tests/lib/fakestore/store/store.go create mode 100644 tests/lib/fakestore/store/store_test.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 100644 tests/lib/pkgdb.sh create mode 100644 tests/lib/prepare-project.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/snapbuild/main.go 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 100644 tests/lib/snaps/basic/meta/icon.png create mode 100644 tests/lib/snaps/basic/meta/snap.yaml 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/complexion/bin/complexion create mode 100644 tests/lib/snaps/complexion/complexion.bash-completer create mode 100644 tests/lib/snaps/complexion/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/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/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-classic-confinement/bin/classic-confinement 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-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 100644 tests/lib/snaps/test-snapd-devmode/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 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 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 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 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 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/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/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/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/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/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-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/find-private/successful_login.exp create mode 100644 tests/main/find-private/task.yaml create mode 100644 tests/main/help/task.yaml create mode 100644 tests/main/i18n/task.yaml create mode 100644 tests/main/install-errors/task.yaml create mode 100644 tests/main/install-hook/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-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-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-cli/task.yaml create mode 100644 tests/main/interfaces-content-empty-content-attr/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-firewall-control/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-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-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/task.yaml create mode 100644 tests/main/interfaces-network-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-process-control/task.yaml create mode 100644 tests/main/interfaces-shutdown-introspection/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-upower-observe/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/manpages/task.yaml create mode 100644 tests/main/media-sharing/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-core-with-failing-configure-hook/task.yaml create mode 100644 tests/main/refresh-core-with-hanging-configure-hook/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/task.yaml create mode 100644 tests/main/security-devpts/pts.exp 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/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-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-multi-service-failing/task.yaml create mode 100644 tests/main/snap-on-non-shared-root/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-seccomp/task.yaml create mode 100644 tests/main/snap-service/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-update-ns/task.yaml create mode 100644 tests/main/snapctl-from-snap/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-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/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/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/unit/c-unit-tests/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 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/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 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..b9f8c3fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +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 +data/info + +# test-driver +*.log +*.trs + +# Automake for the cmd/ parts +cmd/Makefile +cmd/Makefile.in +snap-confine-*.tar.gz +.deps + +# Autoconf +aclocal.m4 +autom4te.cache +compile +config.guess +config.h +config.h.in +config.status +config.sub +configure +depcomp +install-sh +missing +stamp-h1 +test-driver diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..9c6ef23c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +sudo: required +dist: trusty +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: + - curl -s -o - http://canihazip.com/s + - sudo apt-get update -qq + - sudo apt-get install -qq squashfs-tools xdelta3 + - sudo apt-get install -qq gnupg1 || sudo apt-get install -qq gnupg + +script: + - ./run-checks --spread 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..07e28fd4 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,195 @@ +# 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/... + +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. + +# 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/README.md b/README.md new file mode 100644 index 00000000..ad64d3ca --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +[![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) +or on [our mailing list](https://lists.snapcraft.io/mailman/listinfo/snapcraft). + +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 \ No newline at end of file diff --git a/arch/arch.go b/arch/arch.go new file mode 100644 index 00000000..914a24c5 --- /dev/null +++ b/arch/arch.go @@ -0,0 +1,140 @@ +// -*- 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" +) + +// 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", + } + + 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..a6320ff7 --- /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 err == ErrNotFound { + 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 && err != ErrNotFound { + 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 err == ErrNotFound { + 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..832f2b34 --- /dev/null +++ b/asserts/account_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 ( + "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() + 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..c6a5d035 --- /dev/null +++ b/asserts/asserts.go @@ -0,0 +1,973 @@ +// -*- 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} + +// ... +) + +// 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, + // 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] +} + +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 +} + +// 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) { + if len(ref.PrimaryKey) != len(ref.Type.PrimaryKey) { + return nil, fmt.Errorf("%q assertion reference primary key has the wrong length (expected %v): %v", ref.Type.Name, ref.Type.PrimaryKey, ref.PrimaryKey) + } + headers := make(map[string]string, len(ref.PrimaryKey)) + for i, name := range ref.Type.PrimaryKey { + headers[name] = ref.PrimaryKey[i] + } + 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..4324941d --- /dev/null +++ b/asserts/asserts_test.go @@ -0,0 +1,839 @@ +// -*- 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) 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) 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", + "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..3ce89e4a --- /dev/null +++ b/asserts/assertstest/assertstest.go @@ -0,0 +1,355 @@ +// -*- 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) { + 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 + + // 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 +} + +// NewStoreStack creates a new store assertion stack. It panics on error. +func NewStoreStack(authorityID string, rootPrivKey, storePrivKey asserts.PrivateKey) *StoreStack { + rootSigning := NewSigningDB(authorityID, rootPrivKey) + 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, + }, rootPrivKey.PublicKey(), "") + trusted := []asserts.Assertion{trustedAcct, trustedKey} + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: trusted, + }) + if err != nil { + panic(err) + } + err = db.ImportKey(storePrivKey) + if err != nil { + panic(err) + } + storeKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{ + "name": "store", + }, storePrivKey.PublicKey(), "") + err = db.Add(storeKey) + if err != nil { + panic(err) + } + + return &StoreStack{ + TrustedAccount: trustedAcct, + TrustedKey: trustedKey, + Trusted: trusted, + + 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 err == asserts.ErrNotFound { + 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..3c1b0c9b --- /dev/null +++ b/asserts/assertstest/assertstest_test.go @@ -0,0 +1,122 @@ +// -*- 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) { + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + + store := assertstest.NewStoreStack("super", rootPrivKey, storePrivKey) + + 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") + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + }) + 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") + + 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") +} 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..0f9ee2f3 --- /dev/null +++ b/asserts/database.go @@ -0,0 +1,575 @@ +// -*- 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 ( + "errors" + "fmt" + "regexp" + "time" +) + +// 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 ErrNotFound. + 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, ErrNotFound +} + +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) + Trusted []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 +} + +// Well-known errors +var ( + ErrNotFound = errors.New("assertion not found") +) + +// 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 ErrNotFound if the assertion cannot be found. + Find(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 ErrNotFound if the assertion cannot be found. + FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) + // FindMany finds assertions based on arbitrary headers. + // It returns ErrNotFound if no assertion can be found. + FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) + // FindManyTrusted finds assertions in the trusted set based + // on arbitrary headers. It returns ErrNotFound if no + // assertion can be found. + FindManyTrusted(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 + 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("error loading for use 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("error loading for use trusted account %q: %v", acct.DisplayName(), err) + } + default: + return nil, fmt.Errorf("cannot load trusted assertions that are not account-key or account: %s", a.Type().Name) + } + } + + 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, + // order here is relevant, Find* precedence and + // findAccountKey depend on it, trusted should win over the + // general backstore! + backstores: []Backstore{trustedBackstore, 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 err != ErrNotFound { + return nil, err + } + } + return nil, ErrNotFound +} + +// 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 err == ErrNotFound { + 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 && err != ErrNotFound { + 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 err != ErrNotFound { + return fmt.Errorf("cannot add %q assertion with primary key clashing with a trusted 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 := make([]string, len(assertionType.PrimaryKey)) + for i, k := range assertionType.PrimaryKey { + keyVal := headers[k] + if keyVal == "" { + return nil, fmt.Errorf("must provide primary key: %v", k) + } + keyValues[i] = keyVal + } + + var assert Assertion + for _, bs := range backstores { + a, err := bs.Get(assertionType, keyValues, maxFormat) + if err == nil { + assert = a + break + } + if err != ErrNotFound { + return nil, err + } + } + + if assert == nil || !searchMatch(assert, headers) { + return nil, ErrNotFound + } + + return assert, nil +} + +// Find an assertion based on arbitrary headers. +// Provided headers must contain the primary key for the assertion type. +// It returns ErrNotFound 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 ErrNotFound 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) +} + +// 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 ErrNotFound 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, ErrNotFound + } + return res, nil +} + +// FindMany finds assertions based on arbitrary headers. +// It returns ErrNotFound 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) +} + +// FindManyTrusted finds assertions in the trusted set based on +// arbitrary headers. It returns ErrNotFound if no assertion can be +// found. +func (db *Database) FindManyTrusted(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) { + return db.findMany([]Backstore{db.trusted}, 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..48d11b72 --- /dev/null +++ b/asserts/database_test.go @@ -0,0 +1,1012 @@ +// -*- 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 load 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) + + trustedKey := testPrivKey0 + 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) + 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) 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) + + retrieved1, err := safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "b", + }) + c.Assert(err, Equals, asserts.ErrNotFound) + c.Check(retrieved1, IsNil) + + // checking also extra headers + retrieved1, err = safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "a", + "authority-id": "other-auth-id", + }) + c.Assert(err, Equals, asserts.ErrNotFound) + 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) + + res, err = safs.db.FindMany(asserts.TestOnlyType, map[string]string{ + "primary-key": "b", + "other": "other-x", + }) + c.Assert(res, HasLen, 0) + c.Check(err, Equals, asserts.ErrNotFound) +} + +func (safs *signAddFindSuite) TestFindFindsTrustedAccountKeys(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 trusted and indirectly trusted + accKeys, err := safs.db.FindMany(asserts.AccountKeyType, nil) + c.Assert(err, IsNil) + c.Check(accKeys, HasLen, 2) +} + +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 + _, err = safs.db.FindTrusted(asserts.AccountType, map[string]string{ + "account-id": acct1.AccountID(), + }) + c.Check(err, Equals, asserts.ErrNotFound) + + _, err = safs.db.FindTrusted(asserts.AccountKeyType, map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + }) + c.Check(err, Equals, asserts.ErrNotFound) +} + +func (safs *signAddFindSuite) TestFindManyTrusted(c *C) { + 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()), + }, + } + 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.FindManyTrusted(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 multiple trusted keys + tKeys, err := db.FindManyTrusted(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 trusted assertions + _, err = db.FindManyTrusted(asserts.AccountType, map[string]string{ + "account-id": acct1.AccountID(), + }) + c.Check(err, Equals, asserts.ErrNotFound) + + _, err = db.FindManyTrusted(asserts.AccountKeyType, map[string]string{ + "account-id": acct1.AccountID(), + "public-key-sha3-384": acct1Key.PublicKeyID(), + }) + c.Check(err, Equals, asserts.ErrNotFound) +} + +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) 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, Equals, asserts.ErrNotFound) +} + +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.ErrNotFound, 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..74abadca --- /dev/null +++ b/asserts/device_asserts_test.go @@ -0,0 +1,538 @@ +// -*- 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" +) + +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, "brand1", 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, "brand1", 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`}, + {"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) 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..36fd46f1 --- /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 a trusted assertion, in which case + // there is nothing to do + _, err := ref.Resolve(f.db.FindTrusted) + if err == nil { + return nil + } + if err != ErrNotFound { + 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..2f9f2d77 --- /dev/null +++ b/asserts/fetcher_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 ( + "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) { + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey) +} + +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..834248a6 --- /dev/null +++ b/asserts/fsbackstore.go @@ -0,0 +1,220 @@ +// -*- 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 := make([]string, len(assertType.PrimaryKey)) + for i, k := range assertType.PrimaryKey { + primaryPath[i] = assert.HeaderString(k) + } + + 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() + + return fsbs.currentAssertion(assertType, key, maxFormat) +} + +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..ce1ed7e8 --- /dev/null +++ b/asserts/fsbackstore_test.go @@ -0,0 +1,251 @@ +// -*- 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, Equals, asserts.ErrNotFound) + + err = bs.Put(asserts.TestOnlyType, af2) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1) + c.Assert(err, Equals, asserts.ErrNotFound) + + 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..5968207b --- /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", testPrivKey0, testPrivKey1) + + 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..fe744fef --- /dev/null +++ b/asserts/membackstore.go @@ -0,0 +1,184 @@ +// -*- 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 ( + "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 +} + +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+len(assertType.PrimaryKey)) + internalKey[0] = assertType.Name + for i, name := range assertType.PrimaryKey { + internalKey[1+i] = assert.HeaderString(name) + } + + 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) + + return mbs.top.get(internalKey, maxFormat) +} + +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..f755ff81 --- /dev/null +++ b/asserts/membackstore_test.go @@ -0,0 +1,346 @@ +// -*- 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, Equals, asserts.ErrNotFound) + 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, Equals, asserts.ErrNotFound) + 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, Equals, asserts.ErrNotFound) + + err = bs.Put(asserts.TestOnlyType, af2) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"zoo"}, 1) + c.Assert(err, Equals, asserts.ErrNotFound) + + 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..93cd4c2e --- /dev/null +++ b/asserts/repair.go @@ -0,0 +1,134 @@ +// -*- 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 ( + "regexp" + "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 + + 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 "id" of the repair. It should be a short string +// that follows a convention like "REPAIR-123". Similar to a CVE there +// should be a public place to look up details about the repair-id +// (e.g. the snapcraft forum). +func (r *Repair) RepairID() string { + return r.HeaderString("repair-id") +} + +// 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 + } + + if _, err = checkStringMatchesWhat(assert.headers, "repair-id", "header", validRepairID); err != nil { + return nil, 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 + } + 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, + disabled: disabled, + timestamp: timestamp, + }, nil +} diff --git a/asserts/repair_test.go b/asserts/repair_test.go new file mode 100644 index 00000000..bc07366b --- /dev/null +++ b/asserts/repair_test.go @@ -0,0 +1,177 @@ +// -*- 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"+ + "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.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"`}, + {"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"`}, + {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..f4b08665 --- /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 err == ErrNotFound { + 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 err == ErrNotFound { + 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 err == ErrNotFound { + 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 err == ErrNotFound { + 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 err == ErrNotFound { + 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 err == ErrNotFound { + 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 err == ErrNotFound { + 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 err == ErrNotFound { + 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..f875281a --- /dev/null +++ b/asserts/snap_asserts_test.go @@ -0,0 +1,1772 @@ +// -*- 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) (storeDB *assertstest.SigningDB, checkDB *asserts.Database) { + trustedPrivKey := testPrivKey0 + storePrivKey := testPrivKey1 + + store := assertstest.NewStoreStack("canonical", trustedPrivKey, storePrivKey) + cfg := &asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + } + checkDB, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + + // add store key + err = checkDB.Add(store.StoreAccountKey("")) + c.Assert(err, IsNil) + + return store.SigningDB, checkDB +} + +func setup3rdPartySigning(c *C, username string, storeDB *assertstest.SigningDB, 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(nil) + 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(nil) + 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) + + prereqDevAccount(c, storeDB, db) + + headers := vs.makeHeaders(nil) + a, err := storeDB.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) + + prereqDevAccount(c, storeDB, db) + prereqSnapDecl2(c, storeDB, db) + + headers := vs.makeHeaders(nil) + a, err := storeDB.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..99530d3c --- /dev/null +++ b/asserts/snapasserts/snapasserts.go @@ -0,0 +1,144 @@ +// -*- 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 ErrNotFound 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 asserts.ErrNotFound 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..6acc8212 --- /dev/null +++ b/asserts/snapasserts/snapasserts_test.go @@ -0,0 +1,317 @@ +// -*- 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) { + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey) + + 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(err, Equals, asserts.ErrNotFound) +} + +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/sysdb/staging.go b/asserts/sysdb/staging.go new file mode 100644 index 00000000..05eeee32 --- /dev/null +++ b/asserts/sysdb/staging.go @@ -0,0 +1,94 @@ +// -*- 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== +` +) + +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} +} diff --git a/asserts/sysdb/sysdb.go b/asserts/sysdb/sysdb.go new file mode 100644 index 00000000..3806e614 --- /dev/null +++ b/asserts/sysdb/sysdb.go @@ -0,0 +1,48 @@ +// -*- 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(), + } + 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..18e912f4 --- /dev/null +++ b/asserts/sysdb/sysdb_test.go @@ -0,0 +1,147 @@ +// -*- 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 + 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} + + 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) 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) + + // 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..f623f269 --- /dev/null +++ b/asserts/sysdb/trusted.go @@ -0,0 +1,153 @@ +// -*- 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 { + 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/asserts.go b/client/asserts.go new file mode 100644 index 00000000..e5690f27 --- /dev/null +++ b/client/asserts.go @@ -0,0 +1,91 @@ +// -*- 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 +} + +// 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..a3c51c9b --- /dev/null +++ b/client/asserts_test.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 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) 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..7554f769 --- /dev/null +++ b/client/client.go @@ -0,0 +1,559 @@ +// -*- 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" +) + +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 + + // 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 +} + +// 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, + } + } + + 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, + } +} + +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) +} + +// 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} + } + } + + 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 := json.Unmarshal(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..acc609e1 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,498 @@ +// -*- 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) 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..386150e2 --- /dev/null +++ b/client/conf_test.go @@ -0,0 +1,93 @@ +// -*- 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) 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..488973ed --- /dev/null +++ b/client/interfaces.go @@ -0,0 +1,111 @@ +// -*- 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" +) + +// 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"` +} + +// Interfaces contains information about all plugs, slots and their connections +type Interfaces struct { + Plugs []Plug `json:"plugs"` + Slots []Slot `json:"slots"` +} + +// InterfaceMetaData contains meta-data about a given interface type. +type InterfaceMetaData struct { + Description string `json:"description,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"` +} + +// Interfaces returns all plugs, slots and their connections. +func (client *Client) Interfaces() (interfaces Interfaces, err error) { + _, err = client.doSync("GET", "/v2/interfaces", nil, nil, nil, &interfaces) + return +} + +// 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..9487a093 --- /dev/null +++ b/client/interfaces_test.go @@ -0,0 +1,170 @@ +// -*- 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) TestClientInterfacesCallsEndpoint(c *check.C) { + _, _ = cs.cli.Interfaces() + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") +} + +func (cs *clientSuite) TestClientInterfaces(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"} + ] + } + ] + } + }` + interfaces, err := cs.cli.Interfaces() + c.Assert(err, check.IsNil) + c.Check(interfaces, check.DeepEquals, client.Interfaces{ + 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..7f25d784 --- /dev/null +++ b/client/packages.go @@ -0,0 +1,220 @@ +// -*- 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 ( + "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"` + Icon string `json:"icon"` + InstalledSize int64 `json:"installed-size"` + InstallDate time.Time `json:"install-date"` + 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"` + 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"` + Apps []AppInfo `json:"apps"` + Broken string `json:"broken"` + Contact string `json:"contact"` + + Prices map[string]float64 `json:"prices"` + Screenshots []Screenshot `json:"screenshots"` + + // The flattended channel map with $track/$risk + Channels map[string]*snap.ChannelSnapInfo `json:"channels"` + + // The ordered list of tracks that contains channels + Tracks []string +} + +type AppInfo struct { + Name string `json:"name"` + Daemon string `json:"daemon"` +} + +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..33007dd3 --- /dev/null +++ b/client/packages_test.go @@ -0,0 +1,226 @@ +// -*- 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 ( + "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, + "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, + 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", + "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), + 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"}, + }, + }) +} diff --git a/client/snap_op.go b/client/snap_op.go new file mode 100644 index 00000000..be3b2c8f --- /dev/null +++ b/client/snap_op.go @@ -0,0 +1,255 @@ +// -*- 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" + "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) +} + +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..ee14bb01 --- /dev/null +++ b/client/snap_op_test.go @@ -0,0 +1,308 @@ +// -*- 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"}, +} + +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..1ac97bdb --- /dev/null +++ b/cmd/Makefile.am @@ -0,0 +1,409 @@ + +EXTRA_DIST = VERSION snap-confine/PORTING +CLEANFILES = +TESTS = +libexec_PROGRAMS = +dist_man_MANS = +noinst_PROGRAMS = +noinst_LIBRARIES = + +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 + sudo install -D -m 4755 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 + sudo apparmor_parser -r snap-confine/snap-confine.apparmor + +## +## libsnap-confine-private.a +## + +noinst_LIBRARIES += libsnap-confine-private.a + +libsnap_confine_private_a_SOURCES = \ + 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 + +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 = $(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.5 +CLEANFILES += snap-confine/snap-confine.5 +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 = -Wall -Werror $(AM_CFLAGS) +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/%.5: 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 + +# 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 +s (setuid) + chmod 4755 $(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/80-snappy-assign.rules \ + 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 udev rules +install-data-local:: + install -d -m 755 $(DESTDIR)$(shell pkg-config udev --variable=udevdir)/rules.d + install -m 644 $(srcdir)/snap-confine/80-snappy-assign.rules $(DESTDIR)$(shell pkg-config udev --variable=udevdir)/rules.d + +# 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 = -Wall -Werror $(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 = $(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..d3703ef0 --- /dev/null +++ b/cmd/autogen.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Welcome to the Happy Maintainer's Utility Script +set -eux + +# 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-arch --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-ubuntu --enable-static-libcap --enable-static-libapparmor --enable-static-libseccomp" + ;; + *) + extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-ubuntu --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) + # NOTE: we need to disable apparmor as the version on OpenSUSE + # is too old to confine snap-confine and installed snaps + # themselves. This should be changed once all the kernel + # patches find their way into the distribution. + extra_opts="--libexecdir=/usr/lib/snapd --disable-apparmor" + ;; +esac + +echo "Configuring with: $extra_opts" +# shellcheck disable=SC2086 +./configure --enable-maintainer-mode --prefix=/usr $extra_opts diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 00000000..9e8d6ad4 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,212 @@ +// -*- 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 ( + "fmt" + "io/ioutil" + "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 + } + switch release.ReleaseInfo.ID { + case "fedora", "centos", "rhel", "opensuse", "suse", "poky", "arch": + 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) + + // mostly useful for tests + if !osutil.GetenvBool(reExecKey, true) { + logger.Debugf("re-exec disabled by user") + return distroTool + } + + // 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") { + panic("InternalToolPath can only be used from snapd") + } + + if !strings.HasPrefix(exe, dirs.SnapMountDir) { + logger.Noticef("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 os.Unsetenv the for or panic if it cannot do that +func mustUnsetenv(key string) { + if err := os.Unsetenv(key); err != nil { + panic(fmt.Sprintf("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 { + 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 osutil.GetenvBool("SNAP_DID_REEXEC") { + mustUnsetenv("SNAP_DID_REEXEC") + 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) + env := append(os.Environ(), "SNAP_DID_REEXEC=1") + panic(syscallExec(full, os.Args, env)) +} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 00000000..bc529488 --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,301 @@ +// -*- 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/release" + "github.com/snapcore/snapd/testutil" +) + +func Test(t *testing.T) { TestingT(t) } + +type cmdSuite struct { + restoreExec 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.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() +} + +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) { + c.Check(func() { cmd.InternalToolPath("potato") }, PanicMatches, "InternalToolPath can only be used from snapd") +} + +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) + c.Check(s.lastExecEnvv, testutil.Contains, "SNAP_DID_REEXEC=1") +} + +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) + c.Check(s.lastExecEnvv, testutil.Contains, "SNAP_DID_REEXEC=1") +} + +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) { + defer s.mockReExecFor(c, s.newCore, "potato")() + + os.Setenv("SNAP_DID_REEXEC", "1") + defer os.Unsetenv("SNAP_DID_REEXEC") + + 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..f76a39df --- /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-ubuntu], + AS_HELP_STRING([--enable-nvidia-ubuntu], [Support for proprietary nvidia drivers (Ubuntu)]), + [case "${enableval}" in + yes) enable_nvidia_ubuntu=yes ;; + no) enable_nvidia_ubuntu=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-ubuntu]) + esac], [enable_nvidia_ubuntu=no]) +AM_CONDITIONAL([NVIDIA_UBUNTU], [test "x$enable_nvidia_ubuntu" = "xyes"]) + +AS_IF([test "x$enable_nvidia_ubuntu" = "xyes"], [ + AC_DEFINE([NVIDIA_UBUNTU], [1], + [Support for proprietary nvidia drivers (Ubuntu)])]) + +# Enable special support for hosts with proprietary nvidia drivers on Arch. +AC_ARG_ENABLE([nvidia-arch], + AS_HELP_STRING([--enable-nvidia-arch], [Support for proprietary nvidia drivers (Arch)]), + [case "${enableval}" in + yes) enable_nvidia_arch=yes ;; + no) enable_nvidia_arch=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-nvidia-arch]) + esac], [enable_nvidia_arch=no]) +AM_CONDITIONAL([NVIDIA_ARCH], [test "x$enable_nvidia_arch" = "xyes"]) + +AS_IF([test "x$enable_nvidia_arch" = "xyes"], [ + AC_DEFINE([NVIDIA_ARCH], [1], + [Support for proprietary nvidia drivers (Arch)])]) + +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..bd832266 --- /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]; + 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/classic-test.c b/cmd/libsnap-confine-private/classic-test.c new file mode 100644 index 00000000..66dfe06c --- /dev/null +++ b/cmd/libsnap-confine-private/classic-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 "classic.h" +#include "classic.c" + +#include + +// TODO: write some tests diff --git a/cmd/libsnap-confine-private/classic.c b/cmd/libsnap-confine-private/classic.c new file mode 100644 index 00000000..2a3196c3 --- /dev/null +++ b/cmd/libsnap-confine-private/classic.c @@ -0,0 +1,15 @@ +#include "config.h" +#include "classic.h" + +#include + +bool is_running_on_classic_distribution() +{ + // NOTE: keep this list sorted please + return false + || access("/var/lib/dpkg/status", F_OK) == 0 + || access("/var/lib/pacman", F_OK) == 0 + || access("/var/lib/portage", F_OK) == 0 + || access("/var/lib/rpm", F_OK) == 0 + || access("/sbin/procd", F_OK) == 0; +} diff --git a/cmd/libsnap-confine-private/classic.h b/cmd/libsnap-confine-private/classic.h new file mode 100644 index 00000000..8927979a --- /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(); + +#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..7283d2dd --- /dev/null +++ b/cmd/libsnap-confine-private/cleanup-funcs-test.c @@ -0,0 +1,41 @@ +/* + * 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 + +// Test that cleanup functions are applied as expected +static void test_cleanup_sanity() +{ + int called = 0; + void fn(int *ptr) { + called = 1; + } + { + int test __attribute__ ((cleanup(fn))); + test = 0; + test++; + } + g_assert_cmpint(called, ==, 1); +} + +static void __attribute__ ((constructor)) init() +{ + 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..a2d736f0 --- /dev/null +++ b/cmd/libsnap-confine-private/cleanup-funcs.h @@ -0,0 +1,70 @@ +/* + * 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 + +/** + * 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..9ca7e089 --- /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() +{ + 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() +{ + 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + // 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() +{ + 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..2815308e --- /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() +{ + 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() +{ + 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..7a3d14d9 --- /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() +{ + 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..8eee9844 --- /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(); + +#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..b27d0757 --- /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() +{ + 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() +{ + 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 __attribute__ ((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 __attribute__ ((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() +{ + if (g_test_subprocess()) { + sc_enable_sanity_timeout(); + debug("waiting..."); + usleep(4 * 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() +{ + 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..4fb88f1b --- /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() +{ + 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(3); + debug("sanity timeout initialized and set for three seconds"); +} + +void sc_disable_sanity_timeout() +{ + 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 __attribute__ ((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]; + 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() +{ + 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..da609cc8 --- /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(); + +/** + * 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(); + +/** + * 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(); + +#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..f06dd65e --- /dev/null +++ b/cmd/libsnap-confine-private/mount-opt-test.c @@ -0,0 +1,257 @@ +/* + * 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() +{ + char buf[1000]; + 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() +{ + char cmd[10000]; + + // 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]; + char to[PATH_MAX]; + 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() +{ + char cmd[1000]; + + // 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 void test_sc_do_mount() +{ + if (g_test_subprocess()) { + bool broken_mount(struct sc_fault_state *state, void *ptr) { + errno = EACCES; + return true; + } + 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 void test_sc_do_umount() +{ + if (g_test_subprocess()) { + bool broken_umount(struct sc_fault_state *state, void *ptr) { + errno = EACCES; + return true; + } + 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() +{ + 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..6b57f951 --- /dev/null +++ b/cmd/libsnap-confine-private/mount-opt.c @@ -0,0 +1,324 @@ +/* + * 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]; + 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]; + 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]; + const char *mount_cmd = NULL; + + void ensure_mount_cmd() { + if (mount_cmd != NULL) { + return; + } + mount_cmd = sc_mount_cmd(buf, sizeof buf, source, + target, fs_type, mountflags, data); + } + + if (sc_is_debug_enabled()) { +#ifdef SNAP_CONFINE_DEBUG_BUILD + ensure_mount_cmd(); +#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. + ensure_mount_cmd(); + + // 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]; + const char *umount_cmd = NULL; + + void ensure_umount_cmd() { + if (umount_cmd != NULL) { + return; + } + umount_cmd = sc_umount_cmd(buf, sizeof buf, target, flags); + } + + if (sc_is_debug_enabled()) { +#ifdef SNAP_CONFINE_DEBUG_BUILD + ensure_umount_cmd(); +#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. + ensure_umount_cmd(); + + // 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..1fa35b7e --- /dev/null +++ b/cmd/libsnap-confine-private/mountinfo-test.c @@ -0,0 +1,175 @@ +/* + * 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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 __attribute__ ((constructor)) init() +{ + 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); +} diff --git a/cmd/libsnap-confine-private/mountinfo.c b/cmd/libsnap-confine-private/mountinfo.c new file mode 100644 index 00000000..1501582a --- /dev/null +++ b/cmd/libsnap-confine-private/mountinfo.c @@ -0,0 +1,253 @@ +/* + * 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 __attribute__ ((cleanup(sc_cleanup_file))) = NULL; + f = fopen(fname, "rt"); + if (f == NULL) { + free(info); + return NULL; + } + char *line __attribute__ ((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 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; + + void show_buffers() { +#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 + } + + show_buffers(); + + char *parse_next_string_field() { + char *field = &entry->line_buf[0] + offset; + int nscanned = + sscanf(line + offset, "%s %n", field, &offset_delta); + if (nscanned != 1) + return NULL; + offset += offset_delta; + show_buffers(); + return field; + } + if ((entry->root = parse_next_string_field()) == NULL) + goto fail; + if ((entry->mount_dir = parse_next_string_field()) == NULL) + goto fail; + if ((entry->mount_opts = parse_next_string_field()) == 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(); + 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()) == NULL) + goto fail; + if ((entry->mount_source = parse_next_string_field()) == NULL) + goto fail; + if ((entry->super_opts = parse_next_string_field()) == NULL) + goto fail; + show_buffers(); + 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..5ce05bdb --- /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() +{ + 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() +{ + 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..4d288207 --- /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() +{ + 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..beecfebf --- /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(); + +#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..f1ba6efd --- /dev/null +++ b/cmd/libsnap-confine-private/snap-test.c @@ -0,0 +1,192 @@ +/* + * 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() +{ + // 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")); + g_assert_false(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")); +} + +static void test_sc_snap_name_validate() +{ + 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 (int 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 (int 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); + } +} + +static void test_sc_snap_name_validate__respects_error_protocol() +{ + 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() +{ + 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..f95667c8 --- /dev/null +++ b/cmd/libsnap-confine-private/snap.c @@ -0,0 +1,160 @@ +/* + * 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-z](-?[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) +{ + 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() +{ + 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() +{ + // 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() +{ + char buf[5]; + sc_must_snprintf(buf, sizeof buf, "1234"); + g_assert_cmpstr(buf, ==, "1234"); +} + +static void test_sc_must_snprintf__fail() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + char buf[1] = { 0xFF }; + + sc_string_init(buf, sizeof buf); + g_assert_cmpint(buf[0], ==, 0); +} + +static void test_sc_string_init__empty_buf() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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_sc_string_quote() +{ +#define DQ "\"" + char buf[16]; + bool is_tested[256] = { false }; + + void test_quoting_of(int c, const char *expected) { + 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); + + is_tested[c] = true; + } + + // 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(0x00, DQ "" DQ); + test_quoting_of(0x01, DQ "\\x01" DQ); + test_quoting_of(0x02, DQ "\\x02" DQ); + test_quoting_of(0x03, DQ "\\x03" DQ); + test_quoting_of(0x04, DQ "\\x04" DQ); + test_quoting_of(0x05, DQ "\\x05" DQ); + test_quoting_of(0x06, DQ "\\x06" DQ); + test_quoting_of(0x07, DQ "\\x07" DQ); + test_quoting_of(0x08, DQ "\\x08" DQ); + test_quoting_of(0x09, DQ "\\t" DQ); + test_quoting_of(0x0a, DQ "\\n" DQ); + test_quoting_of(0x0b, DQ "\\v" DQ); + test_quoting_of(0x0c, DQ "\\x0c" DQ); + test_quoting_of(0x0d, DQ "\\r" DQ); + test_quoting_of(0x0e, DQ "\\x0e" DQ); + test_quoting_of(0x0f, DQ "\\x0f" DQ); + // block 2: 0x10 - 0x1f + test_quoting_of(0x10, DQ "\\x10" DQ); + test_quoting_of(0x11, DQ "\\x11" DQ); + test_quoting_of(0x12, DQ "\\x12" DQ); + test_quoting_of(0x13, DQ "\\x13" DQ); + test_quoting_of(0x14, DQ "\\x14" DQ); + test_quoting_of(0x15, DQ "\\x15" DQ); + test_quoting_of(0x16, DQ "\\x16" DQ); + test_quoting_of(0x17, DQ "\\x17" DQ); + test_quoting_of(0x18, DQ "\\x18" DQ); + test_quoting_of(0x19, DQ "\\x19" DQ); + test_quoting_of(0x1a, DQ "\\x1a" DQ); + test_quoting_of(0x1b, DQ "\\x1b" DQ); + test_quoting_of(0x1c, DQ "\\x1c" DQ); + test_quoting_of(0x1d, DQ "\\x1d" DQ); + test_quoting_of(0x1e, DQ "\\x1e" DQ); + test_quoting_of(0x1f, DQ "\\x1f" DQ); + // block 3: 0x20 - 0x2f + test_quoting_of(0x20, DQ " " DQ); + test_quoting_of(0x21, DQ "!" DQ); + test_quoting_of(0x22, DQ "\\\"" DQ); + test_quoting_of(0x23, DQ "#" DQ); + test_quoting_of(0x24, DQ "$" DQ); + test_quoting_of(0x25, DQ "%" DQ); + test_quoting_of(0x26, DQ "&" DQ); + test_quoting_of(0x27, DQ "'" DQ); + test_quoting_of(0x28, DQ "(" DQ); + test_quoting_of(0x29, DQ ")" DQ); + test_quoting_of(0x2a, DQ "*" DQ); + test_quoting_of(0x2b, DQ "+" DQ); + test_quoting_of(0x2c, DQ "," DQ); + test_quoting_of(0x2d, DQ "-" DQ); + test_quoting_of(0x2e, DQ "." DQ); + test_quoting_of(0x2f, DQ "/" DQ); + // block 4: 0x30 - 0x3f + test_quoting_of(0x30, DQ "0" DQ); + test_quoting_of(0x31, DQ "1" DQ); + test_quoting_of(0x32, DQ "2" DQ); + test_quoting_of(0x33, DQ "3" DQ); + test_quoting_of(0x34, DQ "4" DQ); + test_quoting_of(0x35, DQ "5" DQ); + test_quoting_of(0x36, DQ "6" DQ); + test_quoting_of(0x37, DQ "7" DQ); + test_quoting_of(0x38, DQ "8" DQ); + test_quoting_of(0x39, DQ "9" DQ); + test_quoting_of(0x3a, DQ ":" DQ); + test_quoting_of(0x3b, DQ ";" DQ); + test_quoting_of(0x3c, DQ "<" DQ); + test_quoting_of(0x3d, DQ "=" DQ); + test_quoting_of(0x3e, DQ ">" DQ); + test_quoting_of(0x3f, DQ "?" DQ); + // block 5: 0x40 - 0x4f + test_quoting_of(0x40, DQ "@" DQ); + test_quoting_of(0x41, DQ "A" DQ); + test_quoting_of(0x42, DQ "B" DQ); + test_quoting_of(0x43, DQ "C" DQ); + test_quoting_of(0x44, DQ "D" DQ); + test_quoting_of(0x45, DQ "E" DQ); + test_quoting_of(0x46, DQ "F" DQ); + test_quoting_of(0x47, DQ "G" DQ); + test_quoting_of(0x48, DQ "H" DQ); + test_quoting_of(0x49, DQ "I" DQ); + test_quoting_of(0x4a, DQ "J" DQ); + test_quoting_of(0x4b, DQ "K" DQ); + test_quoting_of(0x4c, DQ "L" DQ); + test_quoting_of(0x4d, DQ "M" DQ); + test_quoting_of(0x4e, DQ "N" DQ); + test_quoting_of(0x4f, DQ "O" DQ); + // block 6: 0x50 - 0x5f + test_quoting_of(0x50, DQ "P" DQ); + test_quoting_of(0x51, DQ "Q" DQ); + test_quoting_of(0x52, DQ "R" DQ); + test_quoting_of(0x53, DQ "S" DQ); + test_quoting_of(0x54, DQ "T" DQ); + test_quoting_of(0x55, DQ "U" DQ); + test_quoting_of(0x56, DQ "V" DQ); + test_quoting_of(0x57, DQ "W" DQ); + test_quoting_of(0x58, DQ "X" DQ); + test_quoting_of(0x59, DQ "Y" DQ); + test_quoting_of(0x5a, DQ "Z" DQ); + test_quoting_of(0x5b, DQ "[" DQ); + test_quoting_of(0x5c, DQ "\\\\" DQ); + test_quoting_of(0x5d, DQ "]" DQ); + test_quoting_of(0x5e, DQ "^" DQ); + test_quoting_of(0x5f, DQ "_" DQ); + // block 7: 0x60 - 0x6f + test_quoting_of(0x60, DQ "`" DQ); + test_quoting_of(0x61, DQ "a" DQ); + test_quoting_of(0x62, DQ "b" DQ); + test_quoting_of(0x63, DQ "c" DQ); + test_quoting_of(0x64, DQ "d" DQ); + test_quoting_of(0x65, DQ "e" DQ); + test_quoting_of(0x66, DQ "f" DQ); + test_quoting_of(0x67, DQ "g" DQ); + test_quoting_of(0x68, DQ "h" DQ); + test_quoting_of(0x69, DQ "i" DQ); + test_quoting_of(0x6a, DQ "j" DQ); + test_quoting_of(0x6b, DQ "k" DQ); + test_quoting_of(0x6c, DQ "l" DQ); + test_quoting_of(0x6d, DQ "m" DQ); + test_quoting_of(0x6e, DQ "n" DQ); + test_quoting_of(0x6f, DQ "o" DQ); + // block 8: 0x70 - 0x7f + test_quoting_of(0x70, DQ "p" DQ); + test_quoting_of(0x71, DQ "q" DQ); + test_quoting_of(0x72, DQ "r" DQ); + test_quoting_of(0x73, DQ "s" DQ); + test_quoting_of(0x74, DQ "t" DQ); + test_quoting_of(0x75, DQ "u" DQ); + test_quoting_of(0x76, DQ "v" DQ); + test_quoting_of(0x77, DQ "w" DQ); + test_quoting_of(0x78, DQ "x" DQ); + test_quoting_of(0x79, DQ "y" DQ); + test_quoting_of(0x7a, DQ "z" DQ); + test_quoting_of(0x7b, DQ "{" DQ); + test_quoting_of(0x7c, DQ "|" DQ); + test_quoting_of(0x7d, DQ "}" DQ); + test_quoting_of(0x7e, DQ "~" DQ); + test_quoting_of(0x7f, DQ "\\x7f" DQ); + // block 9 (8-bit): 0x80 - 0x8f + test_quoting_of(0x80, DQ "\\x80" DQ); + test_quoting_of(0x81, DQ "\\x81" DQ); + test_quoting_of(0x82, DQ "\\x82" DQ); + test_quoting_of(0x83, DQ "\\x83" DQ); + test_quoting_of(0x84, DQ "\\x84" DQ); + test_quoting_of(0x85, DQ "\\x85" DQ); + test_quoting_of(0x86, DQ "\\x86" DQ); + test_quoting_of(0x87, DQ "\\x87" DQ); + test_quoting_of(0x88, DQ "\\x88" DQ); + test_quoting_of(0x89, DQ "\\x89" DQ); + test_quoting_of(0x8a, DQ "\\x8a" DQ); + test_quoting_of(0x8b, DQ "\\x8b" DQ); + test_quoting_of(0x8c, DQ "\\x8c" DQ); + test_quoting_of(0x8d, DQ "\\x8d" DQ); + test_quoting_of(0x8e, DQ "\\x8e" DQ); + test_quoting_of(0x8f, DQ "\\x8f" DQ); + // block 10 (8-bit): 0x90 - 0x9f + test_quoting_of(0x90, DQ "\\x90" DQ); + test_quoting_of(0x91, DQ "\\x91" DQ); + test_quoting_of(0x92, DQ "\\x92" DQ); + test_quoting_of(0x93, DQ "\\x93" DQ); + test_quoting_of(0x94, DQ "\\x94" DQ); + test_quoting_of(0x95, DQ "\\x95" DQ); + test_quoting_of(0x96, DQ "\\x96" DQ); + test_quoting_of(0x97, DQ "\\x97" DQ); + test_quoting_of(0x98, DQ "\\x98" DQ); + test_quoting_of(0x99, DQ "\\x99" DQ); + test_quoting_of(0x9a, DQ "\\x9a" DQ); + test_quoting_of(0x9b, DQ "\\x9b" DQ); + test_quoting_of(0x9c, DQ "\\x9c" DQ); + test_quoting_of(0x9d, DQ "\\x9d" DQ); + test_quoting_of(0x9e, DQ "\\x9e" DQ); + test_quoting_of(0x9f, DQ "\\x9f" DQ); + // block 11 (8-bit): 0xa0 - 0xaf + test_quoting_of(0xa0, DQ "\\xa0" DQ); + test_quoting_of(0xa1, DQ "\\xa1" DQ); + test_quoting_of(0xa2, DQ "\\xa2" DQ); + test_quoting_of(0xa3, DQ "\\xa3" DQ); + test_quoting_of(0xa4, DQ "\\xa4" DQ); + test_quoting_of(0xa5, DQ "\\xa5" DQ); + test_quoting_of(0xa6, DQ "\\xa6" DQ); + test_quoting_of(0xa7, DQ "\\xa7" DQ); + test_quoting_of(0xa8, DQ "\\xa8" DQ); + test_quoting_of(0xa9, DQ "\\xa9" DQ); + test_quoting_of(0xaa, DQ "\\xaa" DQ); + test_quoting_of(0xab, DQ "\\xab" DQ); + test_quoting_of(0xac, DQ "\\xac" DQ); + test_quoting_of(0xad, DQ "\\xad" DQ); + test_quoting_of(0xae, DQ "\\xae" DQ); + test_quoting_of(0xaf, DQ "\\xaf" DQ); + // block 12 (8-bit): 0xb0 - 0xbf + test_quoting_of(0xb0, DQ "\\xb0" DQ); + test_quoting_of(0xb1, DQ "\\xb1" DQ); + test_quoting_of(0xb2, DQ "\\xb2" DQ); + test_quoting_of(0xb3, DQ "\\xb3" DQ); + test_quoting_of(0xb4, DQ "\\xb4" DQ); + test_quoting_of(0xb5, DQ "\\xb5" DQ); + test_quoting_of(0xb6, DQ "\\xb6" DQ); + test_quoting_of(0xb7, DQ "\\xb7" DQ); + test_quoting_of(0xb8, DQ "\\xb8" DQ); + test_quoting_of(0xb9, DQ "\\xb9" DQ); + test_quoting_of(0xba, DQ "\\xba" DQ); + test_quoting_of(0xbb, DQ "\\xbb" DQ); + test_quoting_of(0xbc, DQ "\\xbc" DQ); + test_quoting_of(0xbd, DQ "\\xbd" DQ); + test_quoting_of(0xbe, DQ "\\xbe" DQ); + test_quoting_of(0xbf, DQ "\\xbf" DQ); + // block 13 (8-bit): 0xc0 - 0xcf + test_quoting_of(0xc0, DQ "\\xc0" DQ); + test_quoting_of(0xc1, DQ "\\xc1" DQ); + test_quoting_of(0xc2, DQ "\\xc2" DQ); + test_quoting_of(0xc3, DQ "\\xc3" DQ); + test_quoting_of(0xc4, DQ "\\xc4" DQ); + test_quoting_of(0xc5, DQ "\\xc5" DQ); + test_quoting_of(0xc6, DQ "\\xc6" DQ); + test_quoting_of(0xc7, DQ "\\xc7" DQ); + test_quoting_of(0xc8, DQ "\\xc8" DQ); + test_quoting_of(0xc9, DQ "\\xc9" DQ); + test_quoting_of(0xca, DQ "\\xca" DQ); + test_quoting_of(0xcb, DQ "\\xcb" DQ); + test_quoting_of(0xcc, DQ "\\xcc" DQ); + test_quoting_of(0xcd, DQ "\\xcd" DQ); + test_quoting_of(0xce, DQ "\\xce" DQ); + test_quoting_of(0xcf, DQ "\\xcf" DQ); + // block 14 (8-bit): 0xd0 - 0xdf + test_quoting_of(0xd0, DQ "\\xd0" DQ); + test_quoting_of(0xd1, DQ "\\xd1" DQ); + test_quoting_of(0xd2, DQ "\\xd2" DQ); + test_quoting_of(0xd3, DQ "\\xd3" DQ); + test_quoting_of(0xd4, DQ "\\xd4" DQ); + test_quoting_of(0xd5, DQ "\\xd5" DQ); + test_quoting_of(0xd6, DQ "\\xd6" DQ); + test_quoting_of(0xd7, DQ "\\xd7" DQ); + test_quoting_of(0xd8, DQ "\\xd8" DQ); + test_quoting_of(0xd9, DQ "\\xd9" DQ); + test_quoting_of(0xda, DQ "\\xda" DQ); + test_quoting_of(0xdb, DQ "\\xdb" DQ); + test_quoting_of(0xdc, DQ "\\xdc" DQ); + test_quoting_of(0xdd, DQ "\\xdd" DQ); + test_quoting_of(0xde, DQ "\\xde" DQ); + test_quoting_of(0xdf, DQ "\\xdf" DQ); + // block 15 (8-bit): 0xe0 - 0xef + test_quoting_of(0xe0, DQ "\\xe0" DQ); + test_quoting_of(0xe1, DQ "\\xe1" DQ); + test_quoting_of(0xe2, DQ "\\xe2" DQ); + test_quoting_of(0xe3, DQ "\\xe3" DQ); + test_quoting_of(0xe4, DQ "\\xe4" DQ); + test_quoting_of(0xe5, DQ "\\xe5" DQ); + test_quoting_of(0xe6, DQ "\\xe6" DQ); + test_quoting_of(0xe7, DQ "\\xe7" DQ); + test_quoting_of(0xe8, DQ "\\xe8" DQ); + test_quoting_of(0xe9, DQ "\\xe9" DQ); + test_quoting_of(0xea, DQ "\\xea" DQ); + test_quoting_of(0xeb, DQ "\\xeb" DQ); + test_quoting_of(0xec, DQ "\\xec" DQ); + test_quoting_of(0xed, DQ "\\xed" DQ); + test_quoting_of(0xee, DQ "\\xee" DQ); + test_quoting_of(0xef, DQ "\\xef" DQ); + // block 16 (8-bit): 0xf0 - 0xff + test_quoting_of(0xf0, DQ "\\xf0" DQ); + test_quoting_of(0xf1, DQ "\\xf1" DQ); + test_quoting_of(0xf2, DQ "\\xf2" DQ); + test_quoting_of(0xf3, DQ "\\xf3" DQ); + test_quoting_of(0xf4, DQ "\\xf4" DQ); + test_quoting_of(0xf5, DQ "\\xf5" DQ); + test_quoting_of(0xf6, DQ "\\xf6" DQ); + test_quoting_of(0xf7, DQ "\\xf7" DQ); + test_quoting_of(0xf8, DQ "\\xf8" DQ); + test_quoting_of(0xf9, DQ "\\xf9" DQ); + test_quoting_of(0xfa, DQ "\\xfa" DQ); + test_quoting_of(0xfb, DQ "\\xfb" DQ); + test_quoting_of(0xfc, DQ "\\xfc" DQ); + test_quoting_of(0xfd, DQ "\\xfd" DQ); + test_quoting_of(0xfe, DQ "\\xfe" DQ); + test_quoting_of(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() +{ + 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_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..23940325 --- /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 || 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..be0e0191 --- /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() +{ + 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() +{ + 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..cf70b084 --- /dev/null +++ b/cmd/libsnap-confine-private/utils-test.c @@ -0,0 +1,174 @@ +/* + * 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_str2bool() +{ + int err; + bool value; + + err = str2bool("yes", &value); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + err = str2bool("1", &value); + g_assert_cmpint(err, ==, 0); + g_assert_true(value); + + err = str2bool("no", &value); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + err = str2bool("0", &value); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + err = str2bool("", &value); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + err = str2bool(NULL, &value); + g_assert_cmpint(err, ==, 0); + g_assert_false(value); + + err = str2bool("flower", &value); + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(errno, ==, EINVAL); + + err = str2bool("yes", NULL); + g_assert_cmpint(err, ==, -1); + g_assert_cmpint(errno, ==, EFAULT); +} + +static void test_die() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + g_test_add_func("/utils/str2bool", test_str2bool); + 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..5d520db8 --- /dev/null +++ b/cmd/libsnap-confine-private/utils.c @@ -0,0 +1,209 @@ +/* + * 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. + * + * 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. + **/ +static int str2bool(const char *text, bool * value) +{ + if (value == NULL) { + errno = EFAULT; + return -1; + } + if (text == NULL) { + *value = false; + return 0; + } + for (int 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 str2bool(), namely "yes", "no" as well as "1" + * and "0". All other values are treated as false and a diagnostic message is + * printed to stderr. + **/ +static bool getenv_bool(const char *name) +{ + const char *str_value = getenv(name); + bool value; + if (str2bool(str_value, &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() +{ + return getenv_bool("SNAP_CONFINE_DEBUG"); +} + +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 __attribute__ ((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 __attribute__ ((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..67909939 --- /dev/null +++ b/cmd/libsnap-confine-private/utils.h @@ -0,0 +1,58 @@ +/* + * 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 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/80-snappy-assign.rules b/cmd/snap-confine/80-snappy-assign.rules new file mode 100644 index 00000000..5285c310 --- /dev/null +++ b/cmd/snap-confine/80-snappy-assign.rules @@ -0,0 +1,2 @@ +# add/remove snap package access to assigned devices +TAG=="snap_*", RUN+="/lib/udev/snappy-app-dev $env{ACTION} $env{TAG} $devpath $major:$minor" 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..f54b2121 --- /dev/null +++ b/cmd/snap-confine/apparmor-support.c @@ -0,0 +1,140 @@ +/* + * 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"); + 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 __attribute__ ((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..b9b5e910 --- /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() +{ + 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]; + 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() +{ + 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() +{ + 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() +{ + 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..03299a30 --- /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]; + 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 __attribute__ ((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]; + 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..df3c6083 --- /dev/null +++ b/cmd/snap-confine/mount-support-nvidia.c @@ -0,0 +1,269 @@ +/* + * 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" + +#ifdef NVIDIA_ARCH + +// 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-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*", +}; + +static const size_t nvidia_globs_len = + sizeof nvidia_globs / sizeof *nvidia_globs; + +// 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 __attribute__ ((__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]; + char symlink_target[512]; + const char *pathname = glob_res.gl_pathv[i]; + char *pathname_copy + __attribute__ ((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_mount_nvidia_driver_arch(const char *rootfs_dir) +{ + // Bind mount a tmpfs on $rootfs_dir/var/lib/snapd/lib/gl + char buf[512]; + sc_must_snprintf(buf, sizeof(buf), "%s%s", rootfs_dir, + "/var/lib/snapd/lib/gl"); + const char *libgl_dir = buf; + 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, nvidia_globs, + nvidia_globs_len); + // Remount .../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); + } +} + +#endif // ifdef NVIDIA_ARCH + +#ifdef NVIDIA_UBUNTU + +struct sc_nvidia_driver { + int major_version; + int minor_version; +}; + +#define SC_NVIDIA_DRIVER_VERSION_FILE "/sys/module/nvidia/version" +#define SC_LIBGL_DIR "/var/lib/snapd/lib/gl" + +static void sc_probe_nvidia_driver(struct sc_nvidia_driver *driver) +{ + FILE *file __attribute__ ((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_mount_nvidia_driver_ubuntu(const char *rootfs_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], dst[PATH_MAX]; + sc_must_snprintf(src, sizeof src, "/usr/lib/nvidia-%d", + driver.major_version); + sc_must_snprintf(dst, sizeof dst, "%s%s", rootfs_dir, SC_LIBGL_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; + } + // Bind mount the binary nvidia driver into /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); + } +} +#endif // ifdef NVIDIA_UBUNTU + +void sc_mount_nvidia_driver(const char *rootfs_dir) +{ +#ifdef NVIDIA_UBUNTU + sc_mount_nvidia_driver_ubuntu(rootfs_dir); +#endif // ifdef NVIDIA_UBUNTU +#ifdef NVIDIA_ARCH + sc_mount_nvidia_driver_arch(rootfs_dir); +#endif // ifdef NVIDIA_ARCH +} 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..da3cf80c --- /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() +{ + 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() +{ + 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() +{ + // 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() +{ + 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..c906459c --- /dev/null +++ b/cmd/snap-confine/mount-support.c @@ -0,0 +1,641 @@ +/* + * 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 "../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() +{ + // 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 as described by snapd. + * + * This function reads /var/lib/snapd/mount/$security_tag.fstab as a fstab(5) file + * and executes the mount requests described there. + * + * Currently only bind mounts are allowed. All bind mounts are read only by + * default though the `rw` flag can be used. + * + * This function is called with the rootfs being "consistent" so that it is + * either the core snap on an all-snap system or the core snap + punched holes + * on a classic system. + **/ +static void sc_setup_mount_profiles(const char *snap_name) +{ + debug("%s: %s", __FUNCTION__, snap_name); + + FILE *desired __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL; + FILE *current __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL; + char profile_path[PATH_MAX]; + + sc_must_snprintf(profile_path, sizeof(profile_path), + "/run/snapd/ns/snap.%s.fstab", snap_name); + debug("opening current mount profile %s", profile_path); + current = setmntent(profile_path, "w"); + if (current == NULL) { + die("cannot open current mount profile: %s", profile_path); + } + + sc_must_snprintf(profile_path, sizeof(profile_path), + "/var/lib/snapd/mount/snap.%s.fstab", snap_name); + debug("opening desired mount profile %s", profile_path); + desired = setmntent(profile_path, "r"); + if (desired == NULL && errno == ENOENT) { + // It is ok for the desired profile to not exist. Note that in this + // case we also "update" the current profile as we already opened and + // truncated it above. + return; + } + if (desired == NULL) { + die("cannot open desired mount profile: %s", profile_path); + } + + struct mntent *m = NULL; + while ((m = getmntent(desired)) != NULL) { + debug("read mount entry\n" + "\tmnt_fsname: %s\n" + "\tmnt_dir: %s\n" + "\tmnt_type: %s\n" + "\tmnt_opts: %s\n" + "\tmnt_freq: %d\n" + "\tmnt_passno: %d", + m->mnt_fsname, m->mnt_dir, m->mnt_type, + m->mnt_opts, m->mnt_freq, m->mnt_passno); + int flags = MS_BIND | MS_RDONLY | MS_NODEV | MS_NOSUID; + debug("initial flags are: bind,ro,nodev,nosuid"); + if (strcmp(m->mnt_type, "none") != 0) { + die("cannot honor mount profile, only 'none' filesystem type is supported"); + } + if (hasmntopt(m, "bind") == NULL) { + die("cannot honor mount profile, the bind mount flag is mandatory"); + } + if (hasmntopt(m, "rw") != NULL) { + flags &= ~MS_RDONLY; + } + sc_do_mount(m->mnt_fsname, m->mnt_dir, NULL, flags, NULL); + if (addmntent(current, m) != 0) { // NOTE: returns 1 on error. + die("cannot append entry to the current mount profile"); + } + } +} + +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; +}; + +/** + * 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]; + char dst[PATH_MAX]; + 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; + 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); + sc_do_mount(src, dst, NULL, MS_BIND, NULL); + sc_do_mount("none", dst, NULL, 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) +{ + int 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; +} + +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 __attribute__ ((cleanup(sc_cleanup_string))) = NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + // Remember if we are on classic, some things behave differently there. + bool on_classic_distro = is_running_on_classic_distribution(); + if (on_classic_distro) { + 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]; + again: + 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"; + goto again; + } + 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, + }; + 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, + }; + 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_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 + __attribute__ ((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() +{ + 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..988e16d5 --- /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(); +#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..720f82aa --- /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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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..14c5e926 --- /dev/null +++ b/cmd/snap-confine/ns-support.c @@ -0,0 +1,477 @@ +/* + * 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 "../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() +{ + struct sc_mountinfo *info + __attribute__ ((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() +{ + int init_mnt_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1; + int self_mnt_fd __attribute__ ((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], self_buf[128]; + 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 + __attribute__ ((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() +{ + 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() +{ + 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); +} + +void sc_create_or_join_ns_group(struct sc_ns_group *group, + struct sc_apparmor *apparmor) +{ + // Open the mount namespace file. + char mnt_fname[PATH_MAX]; + sc_must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, + SC_NS_MNT_FILE); + int mnt_fd __attribute__ ((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 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 buf; + if (fstatfs(mnt_fd, &buf) < 0) { + die("cannot perform fstatfs() on an 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 (buf.f_type == NSFS_MAGIC || buf.f_type == PROC_SUPER_MAGIC) { + char *vanilla_cwd __attribute__ ((cleanup(sc_cleanup_string))) = + NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + 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); + // 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]; + char dst[PATH_MAX]; + 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 __attribute__ ((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]; + 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..6b62260a --- /dev/null +++ b/cmd/snap-confine/ns-support.h @@ -0,0 +1,145 @@ +/* + * 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(); + +/** + * 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(); + +/** + * 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); + +/** + * 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..1be076e1 --- /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() +{ + 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]; + 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 __attribute__ ((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]; + char dest_name[PATH_MAX * 2]; + // 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() +{ + 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() +{ + // 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]; + 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..3d323d73 --- /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(); + +#endif diff --git a/cmd/snap-confine/seccomp-support.c b/cmd/snap-confine/seccomp-support.c new file mode 100644 index 00000000..08a34276 --- /dev/null +++ b/cmd/snap-confine/seccomp-support.c @@ -0,0 +1,217 @@ +/* + * 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. +const int 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 __attribute__ ((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 __attribute__ ((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 __attribute__ ((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]; + 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 + unsigned char bpf[MAX_BPF_SIZE + 1]; // 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); + + 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..38a1f153 --- /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 + +#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() +{ + // 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() +{ + // Test that typical invocation of snap-confine is parsed correctly. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Check that NULL argument parser can be cleaned up + struct sc_error *err __attribute__ ((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() +{ + // Test that typical invocation of snap-confine is parsed correctly. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // 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 __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Test that snap-confine --version is detected. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Check that calling without any arguments is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Check that calling without any arguments is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Check that lack of security tag is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Check that lack of security tag is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Check that unrecognized option switch is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // 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 + __attribute__ ((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() +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + // Check that --base specifies the name of the base snap. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((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() +{ + 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..8c37ea11 --- /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 + * __attribute__((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..5c93982b --- /dev/null +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -0,0 +1,390 @@ +# Author: Jamie Strandboge +#include + +@LIBEXECDIR@/snap-confine (attach_disconnected) { + # We run privileged, so be fanatical about what we include and don't use + # any abstractions + /etc/ld.so.cache r, + /lib/@{multiarch}/ld-*.so mr, + # libc, you are funny + /lib/@{multiarch}/libc{,-[0-9]*}.so* mr, + /lib/@{multiarch}/libpthread{,-[0-9]*}.so* mr, + /lib/@{multiarch}/librt{,-[0-9]*}.so* mr, + /lib/@{multiarch}/libgcc_s.so* mr, + # normal libs in order + /lib/@{multiarch}/libapparmor.so* mr, + /lib/@{multiarch}/libcgmanager.so* mr, + /lib/@{multiarch}/libdl-[0-9]*.so* mr, + /lib/@{multiarch}/libnih.so* mr, + /lib/@{multiarch}/libnih-dbus.so* mr, + /lib/@{multiarch}/libdbus-1.so* mr, + /lib/@{multiarch}/libudev.so* mr, + /usr/lib/@{multiarch}/libseccomp.so* mr, + /lib/@{multiarch}/libseccomp.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, + + # cgroups + 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, + + # querying udev + /etc/udev/udev.conf r, + /sys/devices/**/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, + + # 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, + + # reading mount profiles + /{tmp/snap.rootfs_*/,}var/lib/snapd/mount/*.fstab 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/modules/ -> /tmp/snap.rootfs_*/lib/modules/, + mount options=(rw rslave) -> /tmp/snap.rootfs_*/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/, + # /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@/{,ubuntu-}core/*/etc/ssl/ -> /tmp/snap.rootfs_*/etc/ssl/, + mount options=(rw bind) @SNAP_MOUNT_DIR@/{,ubuntu-}core/*/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/, + + # 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. + + # 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/*/**, + # 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/*/*/**, + + # nvidia handling, glob needs /usr/** and the launcher must be + # able to bind mount the nvidia dir + /sys/module/nvidia/version r, + /usr/** r, + mount options=(rw bind) /usr/lib/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl/, + + # 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, + /run/snapd/ns/*.fstab 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, + + # 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, + /lib/@{multiarch}/ld-*.so mr, + # libc, you are funny + /lib/@{multiarch}/libc{,-[0-9]*}.so* mr, + /lib/@{multiarch}/libpthread{,-[0-9]*}.so* mr, + /lib/@{multiarch}/librt{,-[0-9]*}.so* mr, + /lib/@{multiarch}/libgcc_s.so* mr, + # normal libs in order + /lib/@{multiarch}/libapparmor.so* mr, + /lib/@{multiarch}/libcgmanager.so* mr, + /lib/@{multiarch}/libnih.so* mr, + /lib/@{multiarch}/libnih-dbus.so* mr, + /lib/@{multiarch}/libdbus-1.so* mr, + /lib/@{multiarch}/libudev.so* mr, + /usr/lib/@{multiarch}/libseccomp.so* mr, + /lib/@{multiarch}/libseccomp.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, +} diff --git a/cmd/snap-confine/snap-confine.c b/cmd/snap-confine/snap-confine.c new file mode 100644 index 00000000..1a1d7100 --- /dev/null +++ b/cmd/snap-confine/snap-confine.c @@ -0,0 +1,275 @@ +/* + * 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 "../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. +void sc_maybe_fixup_permissions() +{ + 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"); + } + } +} + +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 __attribute__ ((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 = getuid(); + gid_t real_gid = getgid(); + +#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 __attribute__ ((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 + __attribute__ ((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); + 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_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..7048f7ce --- /dev/null +++ b/cmd/snap-confine/snap-confine.rst @@ -0,0 +1,185 @@ +============== + snap-confine +============== + +----------------------------------------------- +internal tool for confining 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-confine SECURITY_TAG COMMAND [...ARGUMENTS] + +DESCRIPTION +=========== + +The `snap-confine` is a program used internally by `snapd` to construct a +confined execution environment for snap applications. + +OPTIONS +======= + +The `snap-confine` program does not support any options. + +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. + +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` looks for the `/var/lib/snapd/mount/$SECURITY_TAG.fstab` file. +If present it is read, parsed and treated like a 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). + +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` uses the following files: + +`/var/lib/snapd/mount/*.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..1b033d8d --- /dev/null +++ b/cmd/snap-confine/udev-support.c @@ -0,0 +1,208 @@ +/* + * 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" + +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("can not find %s", path); + dev_t devnum = udev_device_get_devnum(d); + udev_device_unref(d); + + int status = 0; + pid_t pid = fork(); + if (pid < 0) { + die("could not fork"); + } + if (pid == 0) { + uid_t real_uid, effective_uid, saved_uid; + if (getresuid(&real_uid, &effective_uid, &saved_uid) != 0) + die("could not find 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("setuid failed"); + char buf[64]; + // pass snappy-add-dev an empty environment so the + // user-controlled environment can't be used to subvert + // snappy-add-dev + char *env[] = { NULL }; + unsigned major = MAJOR(devnum); + unsigned minor = MINOR(devnum); + sc_must_snprintf(buf, sizeof(buf), "%u:%u", major, minor); + 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)); +} + +/* + * 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 (int 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]; + + sc_must_snprintf(cgroup_dir, sizeof(cgroup_dir), + "/sys/fs/cgroup/devices/%s/", security_tag); + + if (mkdir(cgroup_dir, 0755) < 0 && errno != EEXIST) + die("mkdir failed"); + + // move ourselves into it + char cgroup_file[PATH_MAX]; + sc_must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, + "tasks"); + + char buf[128]; + 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 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..c092370c --- /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() +{ + 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() +{ + 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..32f4e2f3 --- /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 setup_user_xdg_runtime_dir(); +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..27356d1b --- /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]; + 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/main.go b/cmd/snap-exec/main.go new file mode 100644 index 00000000..348d02c5 --- /dev/null +++ b/cmd/snap-exec/main.go @@ -0,0 +1,201 @@ +// -*- 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" +) + +// 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 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 snapExecHook(snapApp, revision, opts.Hook) + } + + return snapExecApp(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 +} + +func snapExecApp(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 + } + // strings.Split() is ok here because we validate all app fields + // and the whitelist is pretty strict (see + // snap/validate.go:appContentWhitelist) + cmdArgv := strings.Split(cmdAndArgs, " ") + cmd := cmdArgv[0] + cmdArgs := cmdArgv[1:] + + // build the environment from the yaml + env := append(os.Environ(), osutil.SubstituteEnv(app.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 snapExecHook(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(), 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..0bf1fbd3 --- /dev/null +++ b/cmd/snap-exec/main_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 main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "syscall" + "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" +) + +// 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 + opts.Command = "" + opts.Hook = "" +} + +func (s *snapExecSuite) TearDown(c *C) { + syscallExec = syscall.Exec + dirs.SetRootDir("/") +} + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app cmd-arg1 + 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 := 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 := 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`}, + {cmd: "stop", expected: "stop-app"}, + {cmd: "post-stop", expected: "post-stop-app"}, + } { + cmd, err := 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 = 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 = 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{} + syscallExec = func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + } + + // launch and verify its run the right way + err := snapExecApp("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{} + syscallExec = func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + return nil + } + + // launch and verify it ran correctly + err := snapExecHook("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 := snapExecHook("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 := parseArgs([]string{"--command=shell", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, IsNil) + c.Assert(opts.Command, Equals, "shell") + c.Assert(snapApp, DeepEquals, "snapname.app") + c.Assert(rest, DeepEquals, []string{"--arg1", "arg2"}) +} + +func (s *snapExecSuite) TestSnapExecErrorsOnUnknown(c *C) { + _, _, err := parseArgs([]string{"--command=shell", "--unknown", "snapname.app", "--arg1", "arg2"}) + c.Check(err, ErrorMatches, "unknown flag `unknown'") +} + +func (s *snapExecSuite) TestSnapExecErrorsOnMissingSnapApp(c *C) { + _, _, err := 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 :) + syscallExec = actuallyExec + + // run it + os.Args = []string{"snap-exec", "snapname.app", "foo", "--bar=baz", "foobar"} + err = 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 :) + syscallExec = actuallyExec + + // run it + os.Args = []string{"snap-exec", "--hook=configure", "snapname"} + err := 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{} + syscallExec = func(argv0 string, argv []string, env []string) error { + execArgv0 = argv0 + execArgs = argv + execEnv = env + return nil + } + + // launch and verify its run the right way + err := snapExecApp("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") +} diff --git a/cmd/snap-repair/cmd_run.go b/cmd/snap-repair/cmd_run.go new file mode 100644 index 00000000..5937113b --- /dev/null +++ b/cmd/snap-repair/cmd_run.go @@ -0,0 +1,43 @@ +// -*- 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" +) + +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{} + +func (c *cmdRun) Execute(args []string) error { + fmt.Fprintf(Stdout, "run is not implemented yet\n") + 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..d12081ef --- /dev/null +++ b/cmd/snap-repair/cmd_run_test.go @@ -0,0 +1,33 @@ +// -*- 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) TestRun(c *C) { + err := repair.ParseArgs([]string{"run"}) + c.Check(err, IsNil) + c.Check(r.Stdout(), Equals, "run is not implemented yet\n") + +} diff --git a/cmd/snap-repair/export_test.go b/cmd/snap-repair/export_test.go new file mode 100644 index 00000000..2b58e267 --- /dev/null +++ b/cmd/snap-repair/export_test.go @@ -0,0 +1,26 @@ +// -*- 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 ( + Parser = parser + ParseArgs = parseArgs + Run = run +) diff --git a/cmd/snap-repair/main.go b/cmd/snap-repair/main.go new file mode 100644 index 00000000..f6085d4e --- /dev/null +++ b/cmd/snap-repair/main.go @@ -0,0 +1,78 @@ +// -*- 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/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. +` +) + +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 + } + + 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..a59e5577 --- /dev/null +++ b/cmd/snap-repair/main_test.go @@ -0,0 +1,78 @@ +// -*- 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/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 + + stdout *bytes.Buffer + stderr *bytes.Buffer +} + +func (r *repairSuite) SetUpTest(c *C) { + r.BaseTest.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 +} + +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 the run command") +} + +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-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..0134f6dc --- /dev/null +++ b/cmd/snap-seccomp/main.go @@ -0,0 +1,701 @@ +// -*- 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" + "path/filepath" + "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 { + 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()) + + // FIXME: right now complain mode is the equivalent to + // unrestricted. We'll want to change this once we + // seccomp logging is in order. + // + // special case: unrestricted means we switch to an allow-all + // filter and are done + if line == "@unrestricted" || 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 + dir, err := os.Open(filepath.Dir(out)) + if err != nil { + return err + } + defer dir.Close() + + fout, err := os.Create(out + ".tmp") + if err != nil { + return err + } + defer fout.Close() + if err := secFilter.ExportBPF(fout); err != nil { + return err + } + if err := fout.Sync(); err != nil { + return err + } + if err := os.Rename(out+".tmp", out); err != nil { + return err + } + return dir.Sync() +} + +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_test.go b/cmd/snap-seccomp/main_test.go new file mode 100644 index 00000000..d587040a --- /dev/null +++ b/cmd/snap-seccomp/main_test.go @@ -0,0 +1,607 @@ +// -*- 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 ( + "encoding/binary" + "fmt" + "io" + "math/rand" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "unsafe" + + . "gopkg.in/check.v1" + + "github.com/mvo5/libseccomp-golang" + + // forked from "golang.org/x/net/bpf" + // until https://github.com/golang/go/issues/20556 + "github.com/mvo5/net/bpf" + + "github.com/snapcore/snapd/arch" + main "github.com/snapcore/snapd/cmd/snap-seccomp" + "github.com/snapcore/snapd/release" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type snapSeccompSuite struct{} + +var _ = Suite(&snapSeccompSuite{}) + +func decodeBpfFromFile(p string) ([]bpf.Instruction, error) { + var ops []bpf.Instruction + var rawOp bpf.RawInstruction + + r, err := os.Open(p) + if err != nil { + return nil, err + } + defer r.Close() + + for { + err = binary.Read(r, nativeEndian(), &rawOp) + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + ops = append(ops, rawOp.Disassemble()) + } + + return ops, nil +} + +func parseBpfInput(s string) (*main.SeccompData, error) { + // syscall;arch;arg1,arg2... + l := strings.Split(s, ";") + + var scmpArch seccomp.ScmpArch + if len(l) < 2 || l[1] == "native" { + scmpArch = main.UbuntuArchToScmpArch(arch.UbuntuArchitecture()) + } else { + scmpArch = main.UbuntuArchToScmpArch(l[1]) + } + + sc, err := seccomp.GetSyscallFromNameByArch(l[0], scmpArch) + if err != nil { + return nil, err + } + // libseccomp may return negative numbers here for syscalls that + // are "special" for some reason. there is no "official" way to + // resolve them using the API to the real number. this is why + // we workaround there. + if sc < 0 { + /* -101 is __PNR_socket */ + if sc == -101 && scmpArch == seccomp.ArchX86 { + sc = 359 /* see src/arch-x86.c socket */ + } else if sc == -101 && scmpArch == seccomp.ArchS390X { + sc = 359 /* see src/arch-s390x.c socket */ + } else if sc == -10165 && scmpArch == seccomp.ArchARM64 { + // -10165 is mknod on aarch64 and it is translated + // to mknodat. for our simulation -10165 is fine + // though + } else { + panic(fmt.Sprintf("cannot resolve syscall %v for arch %v, got %v", l[0], l[1], sc)) + } + } + + var syscallArgs [6]uint64 + if len(l) > 2 { + args := strings.Split(l[2], ",") + for i := range args { + // init with random number argument + syscallArgs[i] = (uint64)(rand.Uint32()) + // override if the test specifies a specific number + if nr, err := strconv.ParseUint(args[i], 10, 64); err == nil { + syscallArgs[i] = nr + } else if nr, ok := main.SeccompResolver[args[i]]; ok { + syscallArgs[i] = nr + } + } + } + sd := &main.SeccompData{} + sd.SetArch(main.ScmpArchToSeccompNativeArch(scmpArch)) + sd.SetNr(sc) + sd.SetArgs(syscallArgs) + return sd, nil +} + +// Endianness detection. +func nativeEndian() binary.ByteOrder { + // Credit matt kane, taken from his gosndfile project. + // https://groups.google.com/forum/#!msg/golang-nuts/3GEzwKfRRQw/D1bMbFP-ClAJ + // https://github.com/mkb218/gosndfile + var i int32 = 0x01020304 + u := unsafe.Pointer(&i) + pb := (*byte)(u) + b := *pb + isLittleEndian := b == 0x04 + + if isLittleEndian { + return binary.LittleEndian + } else { + return binary.BigEndian + } +} + +// simulateBpf: +// 1. runs main.Compile() which will catch syntax errors and output to a file +// 2. takes the output file from main.Compile and loads it via +// decodeBpfFromFile +// 3. parses the decoded bpf using the seccomp library and various +// snapd functions +// 4. runs the parsed bpf through a bpf VM +// +// In this manner, in addition to verifying policy syntax we are able to +// unit test the resulting bpf in several ways approximating the kernels +// behaviour (approximating because this parser is not the kernel's seccomp +// parser). +// +// Full testing of applied policy is done elsewhere via spread tests. +func simulateBpf(c *C, seccompWhitelist, bpfInput string, expected int) { + outPath := filepath.Join(c.MkDir(), "bpf") + err := main.Compile([]byte(seccompWhitelist), outPath) + c.Assert(err, IsNil) + + ops, err := decodeBpfFromFile(outPath) + c.Assert(err, IsNil) + + vm, err := bpf.NewVM(ops) + c.Assert(err, IsNil) + + bpfSeccompInput, err := parseBpfInput(bpfInput) + c.Assert(err, IsNil) + + buf2 := (*[64]byte)(unsafe.Pointer(bpfSeccompInput)) + + out, err := vm.Run(buf2[:]) + c.Assert(err, IsNil) + c.Check(out, Equals, expected, Commentf("unexpected result for %q (input %q), got %v expected %v", seccompWhitelist, bpfInput, out, expected)) +} + +func (s *snapSeccompSuite) SetUpSuite(c *C) { + // FIXME: we currently use a fork of x/net/bpf because of: + // https://github.com/golang/go/issues/20556 + // switch to x/net/bpf once we can simulate seccomp bpf there + bpf.VmEndianness = nativeEndian() +} + +func systemUsesSocketcall() bool { + // We need to skip the tests on trusty/i386 and trusty/s390x as + // those are using the socketcall syscall instead of the real + // socket syscall. + // + // See also: + // https://bugs.launchpad.net/ubuntu/+source/glibc/+bug/1576066 + if release.ReleaseInfo.VersionID == "14.04" { + if arch.UbuntuArchitecture() == "i386" || arch.UbuntuArchitecture() == "s390x" { + return true + } + } + return false +} + +// 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 + {"@unrestricted", "execve", main.SeccompRetAllow}, + {"@complain", "execve", main.SeccompRetAllow}, + + // trivial allow + {"read", "read", main.SeccompRetAllow}, + {"read\nwrite\nexecve\n", "write", main.SeccompRetAllow}, + + // trivial denial + {"read", "execve", 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_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}, + + // test_bad_seccomp_filter_args_termios + {"ioctl - TIOCSTI", "ioctl;native;-,TIOCSTI", main.SeccompRetAllow}, + {"ioctl - TIOCSTI", "ioctl;native;-,99", main.SeccompRetKill}, + } { + // skip socket tests if the system uses socketcall instead + // of socket + if strings.Contains(t.seccompWhitelist, "socket") && systemUsesSocketcall() { + continue + } + simulateBpf(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 "!" .*`}, + } { + 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) { + // skip socket tests if the system uses socketcall instead + // of socket + if systemUsesSocketcall() { + c.Skip("cannot run when socketcall() is used") + return + } + + 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" + simulateBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + simulateBpf(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) + simulateBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + simulateBpf(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) + simulateBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + simulateBpf(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) + simulateBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + // bad input + for _, bad := range []string{"quotactl;native;99999", "read;native;"} { + simulateBpf(c, seccompWhitelist, bad, main.SeccompRetKill) + } + } +} + +// ported from test_restrictions_working_args_prctl +func (s *snapSeccompSuite) TestRestrictionsWorkingArgsPrctl(c *C) { + bpf.VmEndianness = nativeEndian() + + 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_SET_ENDIAN", "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) + simulateBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + // bad input + for _, bad := range []string{"prctl;native;99999", "read;native;"} { + simulateBpf(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) + simulateBpf(c, seccompWhitelist, bpfInputGood, main.SeccompRetAllow) + for _, bad := range []string{ + fmt.Sprintf("prctl;native;%s,99999", arg), + "read;native;", + } { + simulateBpf(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}, + } { + simulateBpf(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}, + } { + simulateBpf(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}, + } { + simulateBpf(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}, + } { + simulateBpf(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}, + // on arm64 we add compat armhf + {"arm64", "read", "read;armhf", main.SeccompRetAllow}, + {"arm64", "read", "read;arm64", main.SeccompRetAllow}, + // on ppc64 we add compat powerpc + {"ppc64", "read", "read;powerpc", main.SeccompRetAllow}, + {"ppc64", "read", "read;ppc64", 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 { + simulateBpf(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..ada96cd5 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.c @@ -0,0 +1,187 @@ +/* + * 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 "bootstrap.h" + +#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; + +// read_cmdline reads /proc/self/cmdline into the specified buffer, returning +// number of bytes read. +ssize_t read_cmdline(char* buf, size_t buf_size) +{ + int fd = open("/proc/self/cmdline", O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot open /proc/self/cmdline"; + return -1; + } + memset(buf, 0, buf_size); + ssize_t num_read = read(fd, buf, buf_size); + if (num_read < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot read /proc/self/cmdline"; + } else if (num_read == buf_size) { + bootstrap_errno = 0; + bootstrap_msg = "cannot fit all of /proc/self/cmdline, buffer too small"; + num_read = -1; + } + close(fd); + return num_read; +} + +// find_argv0 scans the command line buffer and looks for the 0st argument. +const char* +find_argv0(char* buf, size_t num_read) +{ + // cmdline is an array of NUL ('\0') separated strings. + size_t argv0_len = strnlen(buf, num_read); + if (argv0_len == num_read) { + // ensure that the buffer is properly terminated. + return NULL; + } + return buf; +} + +// find_snap_name scans the command line buffer and looks for the 1st argument. +// if the 1st argument exists but is empty NULL is returned. +const char* +find_snap_name(char* buf, size_t num_read) +{ + // cmdline is an array of NUL ('\0') separated strings. We can skip over + // the first entry (program name) and look at the second entry, in our case + // it should be the snap name. + size_t argv0_len = strnlen(buf, num_read); + if (argv0_len + 1 >= num_read) { + return NULL; + } + char* snap_name = &buf[argv0_len + 1]; + if (*snap_name == '\0') { + return NULL; + } + return snap_name; +} + +// 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]; + int n = snprintf(buf, sizeof buf, "/run/snapd/ns/%s.mnt", snap_name); + if (n >= sizeof buf || n < 0) { + bootstrap_errno = errno; + bootstrap_msg = "cannot format mount namespace file name"; + return -1; + } + + // Open the mount namespace file, note that we don't specify O_NOFOLLOW as + // that file is always a special, broken symbolic link. + int fd = open(buf, O_RDONLY | O_CLOEXEC); + 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; +} + +// partially_validate_snap_name performs partial validation of the given name. +// The goal is to ensure that there are no / or .. in the name. +int partially_validate_snap_name(const char* snap_name) +{ + // NOTE: neither set bootstrap_{msg,errno} but the return value means that + // bootstrap does nothing. The name is re-validated by golang. + if (strstr(snap_name, "..") != NULL) { + return -1; + } + if (strchr(snap_name, '/') != NULL) { + return -1; + } +} + +// bootstrap prepares snap-update-ns to work in the namespace of the snap given +// on command line. +void bootstrap(void) +{ + // We don't have argc/argv so let's imitate that by reading cmdline + char cmdline[1024]; + memset(cmdline, 0, sizeof cmdline); + ssize_t num_read; + if ((num_read = read_cmdline(cmdline, sizeof cmdline)) < 0) { + return; + } + + // 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. In snapd we can just set the required + // environment variable. + const char* argv0 = find_argv0(cmdline, (size_t)num_read); + if (argv0 == NULL) { + bootstrap_errno = 0; + bootstrap_msg = "argv0 is corrupted"; + return; + } + 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; + } + + // Find the name of the snap by scanning the cmdline. If there's no snap + // name given, just bail out. The go parts will scan this too. + const char* snap_name = find_snap_name(cmdline, (size_t)num_read); + if (snap_name == NULL) { + return; + } + + // Look for known offenders in the snap name (.. and /) and do nothing if + // those are found. The golang code will validate snap name and print a + // proper error message but this just ensures we don't try to open / setns + // anything unusual. + if (partially_validate_snap_name(snap_name) < 0) { + return; + } + + // Switch the mount namespace to that of the snap given on command line. + if (setns_into_snap(snap_name) < 0) { + return; + } +} diff --git a/cmd/snap-update-ns/bootstrap.go b/cmd/snap-update-ns/bootstrap.go new file mode 100644 index 00000000..6adad961 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.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 + +// 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" + +__attribute__((constructor)) static void init(void) { + 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") +) + +// 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)) +} + +// readCmdline is a wrapper around the C function read_cmdline. +func readCmdline(buf []byte) C.ssize_t { + return C.read_cmdline((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(cap(buf))) +} + +// findArgv0 parses the argv-like array and finds the 0st argument. +func findArgv0(buf []byte) *string { + if ptr := C.find_argv0((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))); ptr != nil { + str := C.GoString(ptr) + return &str + } + return nil +} + +// findSnapName parses the argv-like array and finds the 1st argument. +func findSnapName(buf []byte) *string { + if ptr := C.find_snap_name((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))); ptr != nil { + str := C.GoString(ptr) + return &str + } + return nil +} + +// partiallyValidateSnapName checks if snap name is seemingly valid. +// The real part of the validation happens on the go side. +func partiallyValidateSnapName(snapName string) int { + cStr := C.CString(snapName) + return int(C.partially_validate_snap_name(cStr)) +} diff --git a/cmd/snap-update-ns/bootstrap.h b/cmd/snap-update-ns/bootstrap.h new file mode 100644 index 00000000..4ae11368 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap.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 SNAPD_CMD_SNAP_UPDATE_NS_H +#define SNAPD_CMD_SNAP_UPDATE_NS_H + +#define _GNU_SOURCE + +#include + +extern int bootstrap_errno; +extern const char* bootstrap_msg; + +void bootstrap(void); +ssize_t read_cmdline(char* buf, size_t buf_size); +const char* find_argv0(char* buf, size_t num_read); +const char* find_snap_name(char* buf, size_t num_read); +int partially_validate_snap_name(const char* snap_name); + +#endif diff --git a/cmd/snap-update-ns/bootstrap_test.go b/cmd/snap-update-ns/bootstrap_test.go new file mode 100644 index 00000000..a333ddd6 --- /dev/null +++ b/cmd/snap-update-ns/bootstrap_test.go @@ -0,0 +1,108 @@ +// -*- 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 ( + "strings" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" +) + +type bootstrapSuite struct{} + +var _ = Suite(&bootstrapSuite{}) + +func (s *bootstrapSuite) TestReadCmdLine(c *C) { + buf := make([]byte, 1024) + numRead := update.ReadCmdline(buf) + c.Assert(numRead, Not(Equals), -1) + c.Assert(numRead, Not(Equals), 1) + // Individual arguments are separated with NUL byte. + argv := strings.Split(string(buf[0:numRead]), "\x00") + // Smoke test, the actual value looks like + // "/tmp/go-build020699516/github.com/snapcore/snapd/cmd/snap-update-ns/_test/snap-update-ns.test" + c.Assert(strings.HasSuffix(argv[0], "snap-update-ns.test"), Equals, true, Commentf("argv[0] is %q", argv[0])) +} + +// Check that if there is only one argument we return nil. +func (s *bootstrapSuite) TestFindSnapName1(c *C) { + buf := []byte("arg0\x00") + result := update.FindSnapName(buf) + c.Assert(result, Equals, (*string)(nil)) +} + +// Check that if there are multiple arguments we return the 2nd one. +func (s *bootstrapSuite) TestFindSnapName2(c *C) { + buf := []byte("arg0\x00arg1\x00arg2\x00") + result := update.FindSnapName(buf) + c.Assert(result, Not(Equals), (*string)(nil)) + c.Assert(*result, Equals, "arg1") +} + +// Check that if the 1st argument in the buffer is not terminated we don't crash. +func (s *bootstrapSuite) TestFindSnapName3(c *C) { + buf := []byte("arg0") + result := update.FindSnapName(buf) + c.Assert(result, Equals, (*string)(nil)) +} + +// Check that if the 2nd argument in the buffer is not terminated we don't crash. +func (s *bootstrapSuite) TestFindSnapName4(c *C) { + buf := []byte("arg0\x00arg1") + result := update.FindSnapName(buf) + c.Assert(result, Not(Equals), (*string)(nil)) + c.Assert(*result, Equals, "arg1") +} + +// Check that if the 2nd argument an empty string we return NULL. +func (s *bootstrapSuite) TestFindSnapName5(c *C) { + buf := []byte("arg0\x00\x00") + result := update.FindSnapName(buf) + c.Assert(result, Equals, (*string)(nil)) +} + +// Check that if argv0 is returned as expected +func (s *bootstrapSuite) TestFindArgv0(c *C) { + buf := []byte("arg0\x00argv1\x00") + result := update.FindArgv0(buf) + c.Assert(result, NotNil) + c.Assert(*result, Equals, "arg0") +} + +// Check that if argv0 is unterminated we return NULL. +func (s *bootstrapSuite) TestFindArgv0Unterminated(c *C) { + buf := []byte("arg0") + result := update.FindArgv0(buf) + c.Assert(result, Equals, (*string)(nil)) +} + +// Check that PartiallyValidateSnapName rejects "/" and "..". +func (s *bootstrapSuite) TestPartiallyValidateSnapName(c *C) { + c.Assert(update.PartiallyValidateSnapName("hello-world"), Equals, 0) + c.Assert(update.PartiallyValidateSnapName("hello/world"), Equals, -1) + c.Assert(update.PartiallyValidateSnapName("hello..world"), Equals, -1) +} + +// Check that pre-go bootstrap code is disabled while testing. +func (s *bootstrapSuite) TestBootstrapDisabled(c *C) { + c.Assert(update.BootstrapError(), ErrorMatches, "bootstrap is not enabled while testing") +} diff --git a/cmd/snap-update-ns/export_test.go b/cmd/snap-update-ns/export_test.go new file mode 100644 index 00000000..b9edb3db --- /dev/null +++ b/cmd/snap-update-ns/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 main + +var ( + ReadCmdline = readCmdline + FindArgv0 = findArgv0 + FindSnapName = findSnapName + PartiallyValidateSnapName = partiallyValidateSnapName +) diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go new file mode 100644 index 00000000..ec530605 --- /dev/null +++ b/cmd/snap-update-ns/main.go @@ -0,0 +1,127 @@ +// -*- 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/snap" +) + +var opts struct { + Positionals struct { + SnapName string `positional-arg-name:"SNAP_NAME" required:"yes"` + } `positional-args:"true"` +} + +func main() { + if err := run(); err != nil { + fmt.Printf("cannot update snap namespace: %s\n", err) + os.Exit(1) + } +} + +func parseArgs(args []string) error { + parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) + if _, err := parser.ParseArgs(args); err != nil { + return err + } + return snap.ValidateName(opts.Positionals.SnapName) +} + +func run() error { + if err := parseArgs(os.Args[1:]); err != nil { + return err + } + + // 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 { + return 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 lock.Close() + if err := lock.Lock(); err != nil { + return fmt.Errorf("cannot lock mount namespace of snap %q: %s", snapName, err) + } + + // 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) + } + + 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) + } + + // Compute the needed changes and perform each change if needed, collecting + // those that we managed to perform or that were performed already. + changesNeeded := mount.NeededChanges(currentBefore, desired) + var changesMade []mount.Change + for _, change := range changesNeeded { + if change.Action == mount.Keep { + changesMade = append(changesMade, change) + continue + } + if err := change.Perform(); 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.Mount || change.Action == mount.Keep { + currentAfter.Entries = append(currentAfter.Entries, change.Entry) + } + } + if err := currentAfter.Save(currentProfilePath); err != nil { + return fmt.Errorf("cannot save current mount profile of snap %q: %s", snapName, err) + } + return nil +} diff --git a/cmd/snap-update-ns/main_test.go b/cmd/snap-update-ns/main_test.go new file mode 100644 index 00000000..8b17d3ce --- /dev/null +++ b/cmd/snap-update-ns/main_test.go @@ -0,0 +1,32 @@ +// -*- 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 ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type snapUpdateNsSuite struct{} + +var _ = Suite(&snapUpdateNsSuite{}) 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..b6e3ffca --- /dev/null +++ b/cmd/snap/cmd_ack.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 + +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{{ + name: i18n.G(""), + 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..726d20d3 --- /dev/null +++ b/cmd/snap/cmd_alias.go @@ -0,0 +1,114 @@ +// -*- 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 string `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: ""}, + {name: i18n.G("")}, + }) +} + +func (x *cmdAlias) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName, appName := snap.SplitSnapApp(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..51b687fa --- /dev/null +++ b/cmd/snap/cmd_auto_import_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 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() + + l, err := logger.New(s.stderr, 0) + c.Assert(err, IsNil) + logger.SetLogger(l) + + 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(s.Stderr(), 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() + + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + + l, err := logger.New(s.stderr, 0) + c.Assert(err, IsNil) + logger.SetLogger(l) + + 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(s.Stderr(), 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) + } + + }) + + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + + 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) + + l, err := logger.New(s.stderr, 0) + c.Assert(err, IsNil) + logger.SetLogger(l) + + 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(s.Stderr(), 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() + + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + + l, err := logger.New(s.stderr, 0) + c.Assert(err, IsNil) + logger.SetLogger(l) + + // 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..c1b33261 --- /dev/null +++ b/cmd/snap/cmd_buy.go @@ -0,0 +1,137 @@ +// -*- 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: "", + 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_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..5fbdf31a --- /dev/null +++ b/cmd/snap/cmd_connect.go @@ -0,0 +1,85 @@ +// -*- 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{ + {name: i18n.G(":")}, + {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..b5bc8491 --- /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 fortestingInterfaceList = client.Interfaces{ + 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": fortestingInterfaceList, + }) + 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..63ae5d27 --- /dev/null +++ b/cmd/snap/cmd_create_key.go @@ -0,0 +1,86 @@ +// -*- 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{{ + name: i18n.G(""), + 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..17a6318d --- /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: noun + name: i18n.G(""), + // TRANSLATORS: 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..2f8757bc --- /dev/null +++ b/cmd/snap/cmd_delete_key.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 ( + "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{{ + name: i18n.G(""), + 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..242671b1 --- /dev/null +++ b/cmd/snap/cmd_disconnect.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 ( + "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{ + {name: i18n.G(":")}, + {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..7a7e088d --- /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": fortestingInterfaceList, + }) + 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..324a90e7 --- /dev/null +++ b/cmd/snap/cmd_download.go @@ -0,0 +1,133 @@ +// -*- 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: "", + 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..d2297001 --- /dev/null +++ b/cmd/snap/cmd_export_key.go @@ -0,0 +1,95 @@ +// -*- 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{{ + name: i18n.G(""), + 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..18082b3f --- /dev/null +++ b/cmd/snap/cmd_export_key_test.go @@ -0,0 +1,86 @@ +// -*- 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) { + rootPrivKey, _ := assertstest.GenerateKey(1024) + storePrivKey, _ := assertstest.GenerateKey(752) + storeSigning := assertstest.NewStoreStack("canonical", rootPrivKey, storePrivKey) + 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..889d964b --- /dev/null +++ b/cmd/snap/cmd_find.go @@ -0,0 +1,143 @@ +// -*- 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/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 { + 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{{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..b09ce607 --- /dev/null +++ b/cmd/snap/cmd_get.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 main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +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 + Keys []string + } `positional-args:"yes" required:"yes"` + + Typed bool `short:"t"` + Document bool `short:"d"` +} + +func init() { + addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} }, + map[string]string{ + "d": i18n.G("Always return document, even with single key"), + "t": i18n.G("Strict typing with nulls and quoted strings"), + }, []argDesc{ + { + name: "", + desc: i18n.G("The snap whose conf is being requested"), + }, + { + name: i18n.G(""), + desc: i18n.G("Key of interest within the configuration"), + }, + }) +} + +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") + } + + snapName := string(x.Positional.Snap) + confKeys := x.Positional.Keys + + cli := Client() + conf, err := cli.Conf(snapName, confKeys) + if err != nil { + return err + } + + var confToPrint interface{} = conf + if !x.Document && len(confKeys) == 1 { + confToPrint = conf[confKeys[0]] + } + + if x.Typed && confToPrint == nil { + fmt.Fprintln(Stdout, "null") + return nil + } + + if s, ok := confToPrint.(string); ok && !x.Typed { + fmt.Fprintln(Stdout, s) + return nil + } + + var bytes []byte + if confToPrint != nil { + bytes, err = json.MarshalIndent(confToPrint, "", "\t") + if err != nil { + return err + } + } + + fmt.Fprintln(Stdout, string(bytes)) + return nil +} 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..d716712a --- /dev/null +++ b/cmd/snap/cmd_get_test.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_test + +import ( + "fmt" + "net/http" + "strings" + + . "gopkg.in/check.v1" + + snapset "github.com/snapcore/snapd/cmd/snap" +) + +var getTests = []struct { + args, stdout, error string +}{{ + 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 snapname test-key1 test-key2", + stdout: "{\n\t\"test-key1\": \"test-value1\",\n\t\"test-key2\": 2\n}\n", +}} + +func (s *SnapSuite) TestSnapGetTests(c *C) { + s.mockGetConfigServer(c) + + for _, test := range getTests { + s.stdout.Truncate(0) + s.stderr.Truncate(0) + + c.Logf("Test: %s", test.args) + + _, 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.Stdout(), Equals, test.stdout) + } + } +} + +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 "missing-key": + fmt.Fprintln(w, `{"type":"sync", "status-code": 200, "result": {}}`) + default: + c.Errorf("unexpected keys %q", query.Get("keys")) + } + }) +} 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..8a9d30e9 --- /dev/null +++ b/cmd/snap/cmd_info.go @@ -0,0 +1,323 @@ +// -*- 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" + + "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%q\n", 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: +// - 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(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.Daemon != "" { + 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) + } +} + +// 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 i, 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) + } + // add separator between tracks + if i < len(remote.Tracks)-1 { + fmt.Fprintf(w, " \t\t\t\t\n") + } + } +} + +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%q\n", 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) + + 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) + } + } 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..8aa7fcb4 --- /dev/null +++ b/cmd/snap/cmd_info_test.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_test + +import ( + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +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.Matches, `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, "") +} diff --git a/cmd/snap/cmd_interfaces.go b/cmd/snap/cmd_interfaces.go new file mode 100644 index 00000000..f32c1650 --- /dev/null +++ b/cmd/snap/cmd_interfaces.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 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{{ + name: i18n.G(":"), + 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().Interfaces() + 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..1d5369e2 --- /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) TestInterfacesZeroSlotsOnePlug(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.Interfaces{ + 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) TestInterfacesZeroPlugsOneSlot(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.Interfaces{ + 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) TestInterfacesOneSlotOnePlug(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.Interfaces{ + 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) TestInterfacesTwoPlugs(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.Interfaces{ + 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) TestInterfacesPlugsWithCommonName(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.Interfaces{ + 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) TestInterfacesOsSnapSlots(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.Interfaces{ + 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) TestInterfacesTwoSlotsAndFiltering(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.Interfaces{ + 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) TestInterfacesOfSpecificSnap(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.Interfaces{ + 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) TestInterfacesOfSpecificSnapAndSlot(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.Interfaces{ + 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) TestInterfacesNothingAtAll(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.Interfaces{}, + }) + }) + 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) TestInterfacesOfSpecificType(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.Interfaces{ + 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) TestInterfacesCompletion(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": fortestingInterfaceList, + }) + 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..107bf9cb --- /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 string `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{ + { + name: i18n.G(""), + desc: i18n.G("Assertion type name"), + }, { + name: i18n.G("
"), + 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 := make([]string, len(at.PrimaryKey)) + for i, k := range at.PrimaryKey { + pk, ok := headers[k] + if !ok { + return nil, fmt.Errorf("missing primary header %q to query remote assertion", k) + } + primaryKeys[i] = pk + } + + 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(x.KnownOptions.AssertTypeName, headers) + } else { + assertions, err = Client().Known(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..6ebee7da --- /dev/null +++ b/cmd/snap/cmd_known_test.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_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + + "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, err := url.Parse(server.URL + "/assertions/") + c.Assert(err, check.IsNil) + cfg.AssertionsURI = serverURL + return store.New(cfg, auth) + }) + defer restorer() + + n := 0 + server = httptest.NewServer(http.HandlerFunc(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, "/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, `missing primary header "model" to query remote assertion`) +} 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..1bb4f43c --- /dev/null +++ b/cmd/snap/cmd_login.go @@ -0,0 +1,128 @@ +// -*- 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: noun + name: i18n.G(""), + 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_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..5f9ce868 --- /dev/null +++ b/cmd/snap/cmd_prepare_image.go @@ -0,0 +1,73 @@ +// -*- 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{ + { + name: i18n.G(""), + desc: i18n.G("The model assertion name"), + }, { + name: i18n.G(""), + 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_run.go b/cmd/snap/cmd_run.go new file mode 100644 index 00000000..b31ea72e --- /dev/null +++ b/cmd/snap/cmd_run.go @@ -0,0 +1,420 @@ +// -*- 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" + "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" + } + + return snapRunApp(snapApp, x.Command, args) +} + +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") + } + 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..7f4f361f --- /dev/null +++ b/cmd/snap/cmd_run_test.go @@ -0,0 +1,568 @@ +// -*- 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" + + "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) { + dirs.SetRootDir(c.MkDir()) + + // 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + 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) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "current", dirs.CoreLibExecDir))() + + 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) { + // mock installed snap; happily this also gives us a directory + // below /tmp which the Xauthority migration expects. + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + 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) + + 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) +} diff --git a/cmd/snap/cmd_set.go b/cmd/snap/cmd_set.go new file mode 100644 index 00000000..5a087f38 --- /dev/null +++ b/cmd/snap/cmd_set.go @@ -0,0 +1,94 @@ +// -*- 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" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +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: "", + desc: i18n.G("The snap to configure (e.g. hello-world)"), + }, { + name: i18n.G(""), + 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{} + err := json.Unmarshal([]byte(parts[1]), &value) + if err == nil { + patchValues[parts[0]] = value + } else { + // Not valid JSON-- just save the string as-is. + patchValues[parts[0]] = parts[1] + } + } + + 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..676c1b42 --- /dev/null +++ b/cmd/snap/cmd_set_test.go @@ -0,0 +1,114 @@ +// -*- 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" + + snapset "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "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 + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + 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 + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + snaptest.MockSnap(c, string(validApplyYaml), string(validApplyContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // and mock the server + s.mockSetConfigServer(c, 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) TestSnapSetIntegrationJson(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + 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..88e9b4f1 --- /dev/null +++ b/cmd/snap/cmd_shell.go @@ -0,0 +1,98 @@ +// -*- 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{{ + name: i18n.G(""), + 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..815501bc --- /dev/null +++ b/cmd/snap/cmd_sign_build.go @@ -0,0 +1,115 @@ +// -*- 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{{ + name: i18n.G(""), + 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..572aa4cf --- /dev/null +++ b/cmd/snap/cmd_snap_op.go @@ -0,0 +1,974 @@ +// -*- 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.NewTextProgress() + 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.Progress.Label, 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 != "" { + fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' installed\n"), snap.Name, channelStr, snap.Version, snap.Developer) + } else { + fmt.Fprintf(Stdout, i18n.G("%s%s %s installed\n"), snap.Name, channelStr, snap.Version) + } + case "refresh": + if snap.Developer != "" { + fmt.Fprintf(Stdout, i18n.G("%s%s %s from '%s' refreshed\n"), snap.Name, channelStr, snap.Version, snap.Developer) + } else { + fmt.Fprintf(Stdout, i18n.G("%s%s %s refreshed\n"), snap.Name, channelStr, snap.Version) + } + default: + fmt.Fprintf(Stdout, "internal error, unknown op %q", op) + } + } + 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 + } + + // 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] + fmt.Fprintf(Stdout, i18n.G("%s reverted to %s\n"), name, snap.Version) + 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) +} diff --git a/cmd/snap/cmd_snap_op_test.go b/cmd/snap/cmd_snap_op_test.go new file mode 100644 index 00000000..2b50830b --- /dev/null +++ b/cmd/snap/cmd_snap_op_test.go @@ -0,0 +1,1011 @@ +// -*- 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" + "os" + "path/filepath" + "regexp" + "time" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +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) { + 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() + + d := c.MkDir() + oldStdout := os.Stdout + stdout, err := ioutil.TempFile(d, "stdout") + c.Assert(err, check.IsNil) + defer func() { + os.Stdout = oldStdout + stdout.Close() + os.Remove(stdout.Name()) + }() + os.Stdout = stdout + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + c.Assert(chg, check.IsNil) + c.Assert(err, check.NotNil) + buf, err := ioutil.ReadFile(stdout.Name()) + c.Assert(err, check.IsNil) + c.Check(string(buf), check.Matches, "(?ms).*Waiting for server to restart.*") +} + +func (s *SnapOpSuite) TestWaitRecovers(c *check.C) { + 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"}}`) + }) + + d := c.MkDir() + oldStdout := os.Stdout + stdout, err := ioutil.TempFile(d, "stdout") + c.Assert(err, check.IsNil) + defer func() { + os.Stdout = oldStdout + stdout.Close() + os.Remove(stdout.Name()) + }() + os.Stdout = stdout + + cli := snap.Client() + chg, err := snap.Wait(cli, "x") + // we got the change + c.Assert(chg, check.NotNil) + c.Assert(err, check.IsNil) + buf, err := ioutil.ReadFile(stdout.Name()) + c.Assert(err, check.IsNil) + + // but only after recovering + c.Check(string(buf), check.Matches, "(?ms).*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) 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", "."}, + } + + for _, cmd := range cmds { + s.RedirectClientToTestServer(s.srv.handle) + 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() + } +} diff --git a/cmd/snap/cmd_unalias.go b/cmd/snap/cmd_unalias.go new file mode 100644 index 00000000..c19fe387 --- /dev/null +++ b/cmd/snap/cmd_unalias.go @@ -0,0 +1,67 @@ +// -*- 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 string `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{ + {name: i18n.G("")}, + }) +} + +func (x *cmdUnalias) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + id, err := cli.Unalias(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_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..52727f74 --- /dev/null +++ b/cmd/snap/cmd_watch_test.go @@ -0,0 +1,85 @@ +// -*- 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" + "time" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/testutil" +) + +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) { + 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++ + }) + + oldStdout := os.Stdout + stdout, err := ioutil.TempFile("", "stdout") + c.Assert(err, IsNil) + defer func() { + os.Stdout = oldStdout + stdout.Close() + os.Remove(stdout.Name()) + }() + os.Stdout = stdout + + _, err = snap.Parser().ParseArgs([]string{"watch", "42"}) + os.Stdout = oldStdout + c.Assert(err, IsNil) + c.Check(n, Equals, 3) + + buf, err := ioutil.ReadFile(stdout.Name()) + c.Assert(err, IsNil) + c.Check(string(buf), testutil.Contains, "\rmy-snap 50.00 KB / 100.00 KB") +} 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..d3a50487 --- /dev/null +++ b/cmd/snap/complete.go @@ -0,0 +1,292 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/client" +) + +type installedSnapName string + +func (s installedSnapName) Complete(match string) []flags.Completion { + cli := Client() + snaps, err := cli.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 +} + +type remoteSnapName string + +func (s remoteSnapName) Complete(match string) []flags.Completion { + if len(match) < 3 { + return nil + } + cli := Client() + snaps, _, err := cli.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 { + cli := Client() + changes, err := cli.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 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. + cli := Client() + ifaces, err := cli.Interfaces() + 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 +} diff --git a/cmd/snap/error.go b/cmd/snap/error.go new file mode 100644 index 00000000..9b53f9fc --- /dev/null +++ b/cmd/snap/error.go @@ -0,0 +1,170 @@ +// -*- 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 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..d09fdacc --- /dev/null +++ b/cmd/snap/export_test.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 main + +import ( + "os/user" + "time" + + "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 +) + +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) +} 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..afb6e8f5 --- /dev/null +++ b/cmd/snap/last.go @@ -0,0 +1,85 @@ +// -*- 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{{ + name: i18n.G(""), + 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..aee305e6 --- /dev/null +++ b/cmd/snap/main.go @@ -0,0 +1,330 @@ +// -*- 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" + + "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" +) + +func init() { + // set User-Agent for when 'snap' talks to the store directly (snap download etc...) + httputil.SetUserAgentFromVersion(cmd.Version, "snap") +} + +// Standard streams, redirected for testing. +var ( + Stdin io.Reader = os.Stdin + Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + ReadPassword = terminal.ReadPassword +) + +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 { + if !unicode.IsUpper(([]rune)(desc)[0]) { + logger.Panicf("description of %s's %q not uppercase: %q", cmdName, optName, desc) + } + } +} + +func lintArg(cmdName, optName, desc, origDesc string) { + lintDesc(cmdName, optName, desc, origDesc) + if optName[0] != '<' || optName[len(optName)-1] != '>' { + logger.Panicf("argument %q's %q should have <>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, +} + +// 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..baf64075 --- /dev/null +++ b/cmd/snap/main_test.go @@ -0,0 +1,279 @@ +// -*- 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" + "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) + 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) +} + +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) + 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) + 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) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("/") + + 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..51ae7f38 --- /dev/null +++ b/cmd/snap/notes.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 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 +} + +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 != "", + } +} + +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 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..c5ca764e --- /dev/null +++ b/cmd/snap/notes_test.go @@ -0,0 +1,100 @@ +// -*- 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) 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) +} 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..50564e10 --- /dev/null +++ b/cmd/snapctl/main.go @@ -0,0 +1,68 @@ +// -*- 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() { + 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..04ed283e --- /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() +{ + 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..ab590afd --- /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(); + +__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/daemon/api.go b/daemon/api.go new file mode 100644 index 00000000..b32f9c16 --- /dev/null +++ b/daemon/api.go @@ -0,0 +1,2515 @@ +// -*- 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/dumb" + "github.com/snapcore/snapd/interfaces" + "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/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" +) + +var api = []*Command{ + rootCmd, + sysInfoCmd, + loginCmd, + logoutCmd, + appIconCmd, + findCmd, + snapsCmd, + snapCmd, + snapConfCmd, + interfacesCmd, + assertsCmd, + assertsFindManyCmd, + stateChangeCmd, + stateChangesCmd, + createUserCmd, + buyCmd, + readyToBuyCmd, + snapctlCmd, + usersCmd, + sectionsCmd, + aliasesCmd, + 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, + } + + 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, + GET: getSnapsInfo, + POST: postSnaps, + } + + snapCmd = &Command{ + Path: "/v2/snaps/{name}", + UserOK: true, + GET: getSnapInfo, + POST: postSnap, + } + + snapConfCmd = &Command{ + Path: "/v2/snaps/{name}/conf", + GET: getSnapConf, + PUT: setSnapConf, + } + + interfacesCmd = &Command{ + Path: "/v2/interfaces", + UserOK: true, + GET: getInterfaces, + POST: changeInterfaces, + } + + // TODO: allow to post assertions for UserOK? they are verified anyway + assertsCmd = &Command{ + Path: "/v2/assertions", + POST: doAssert, + } + + assertsFindManyCmd = &Command{ + Path: "/v2/assertions/{assertType}", + UserOK: true, + GET: assertsFindMany, + } + + stateChangeCmd = &Command{ + Path: "/v2/changes/{id}", + UserOK: true, + 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 := snapMgr.RefreshSchedule() + 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(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 map[string]interface{}, resource string) map[string]interface{} { + result["resource"] = resource + + icon, ok := result["icon"].(string) + if !ok || icon == "" || strings.HasPrefix(icon, "http") { + return result + } + result["icon"] = "" + + route := appIconCmd.d.router.Get(appIconCmd.Path) + if route != nil { + name, _ := result["name"].(string) + url, err := route.URL("name", 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: + 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: + 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) + if err != nil { + if err == store.ErrSnapNotFound { + return SnapNotFound(err) + } + 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.NullProgress + 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 ( + snapstateCoreInfo = snapstate.CoreInfo + 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 + + assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations +) + +func ensureStateSoonImpl(st *state.State) { + st.EnsureBefore(0) +} + +var ensureStateSoon = ensureStateSoonImpl + +var errNothingToInstall = errors.New("nothing to install") + +const oldDefaultSnapCoreName = "ubuntu-core" +const defaultCoreSnapName = "core" + +func ensureCore(st *state.State, targetSnap string, userID int) (*state.TaskSet, error) { + if targetSnap == defaultCoreSnapName || targetSnap == oldDefaultSnapCoreName { + return nil, errNothingToInstall + } + + _, err := snapstateCoreInfo(st) + if err != state.ErrNoState { + return nil, err + } + + return snapstateInstall(st, defaultCoreSnapName, "stable", snap.R(0), userID, snapstate.Flags{}) +} + +func withEnsureCore(st *state.State, targetSnap string, userID int, install func() (*state.TaskSet, error)) ([]*state.TaskSet, error) { + ubuCoreTs, err := ensureCore(st, targetSnap, userID) + if err != nil && err != errNothingToInstall { + return nil, err + } + + ts, err := install() + if err != nil { + return nil, err + } + + // ensure main install waits on ubuntu core install + if ubuCoreTs != nil { + ts.WaitAll(ubuCoreTs) + return []*state.TaskSet{ubuCoreTs, ts}, nil + } + + return []*state.TaskSet{ts}, nil +} + +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) + + tsets, err := withEnsureCore(st, inst.Snaps[0], inst.userID, + func() (*state.TaskSet, error) { + return 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, tsets, 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 +} + +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, +} + +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(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) + } + + var userID int + if user != nil { + userID = user.ID + } + tsets, err := withEnsureCore(st, info.Name(), userID, + func() (*state.TaskSet, error) { + return 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, tsets, []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 err { + case nil: + snapName = si.RealName + sideInfo = si + case asserts.ErrNotFound: + // 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) + } + + var userID int + if user != nil { + userID = user.ID + } + + tsets, err := withEnsureCore(st, snapName, userID, + func() (*state.TaskSet, error) { + return snapstateInstallPath(st, sideInfo, tempPath, "", flags) + }, + ) + if err != nil { + return InternalError("cannot install snap file: %v", err) + } + + chg := newChange(st, "install-snap", msg, tsets, []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(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")) + if len(keys) == 0 { + return BadRequest("cannot obtain configuration: no keys supplied") + } + + s := c.d.overlord.State() + s.Lock() + tr := config.NewTransaction(s) + s.Unlock() + + currentConfValues := make(map[string]interface{}) + for _, key := range keys { + var value interface{} + if err := tr.Get(snapName, key, &value); err != nil { + if config.IsNoOption(err) { + return BadRequest("%v", err) + } else { + return InternalError("%v", err) + } + } + + 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{} + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&patchValues); err != nil { + return BadRequest("cannot decode request body into patch values: %v", err) + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + var snapst snapstate.SnapState + if err := snapstate.Get(st, snapName, &snapst); err != nil { + if err == state.ErrNoState { + return SnapNotFound(err) + } else { + return InternalError("%v", err) + } + } + + taskset := configstate.Configure(st, snapName, patchValues, 0) + + 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()}) +} + +// getInterfaces returns all plugs and slots. +func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { + repo := c.d.overlord.InterfaceManager().Repository() + return SyncResponse(repo.Interfaces(), 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"` +} + +// 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 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 err == asserts.ErrNotFound { + 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 ( + postCreateUserUcrednetGetUID = ucrednetGetUID + 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 && err != asserts.ErrNotFound { + 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 := postCreateUserUcrednetGetUID(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) + 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 + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&snapctlOptions); err != nil { + return BadRequest("cannot decode snapctl request: %s", err) + } + + if len(snapctlOptions.Args) == 0 { + return BadRequest("snapctl cannot run without args") + } + + // Right now snapctl is only used for hooks. If at some point it grows + // beyond that, this probably shouldn't go straight to the HookManager. + context, err := c.d.overlord.HookManager().Context(snapctlOptions.ContextID) + if err != nil { + return BadRequest("error running snapctl: %s", err) + } + 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.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 := postCreateUserUcrednetGetUID(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 := st.NewChange(a.Action, summary) + change.Set("snap-names", []string{a.Snap}) + change.AddAll(taskset) + + 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) +} 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..2d402da9 --- /dev/null +++ b/daemon/api_test.go @@ -0,0 +1,5516 @@ +// -*- 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" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/url" + "os" + "os/user" + "path/filepath" + "sort" + "strings" + "time" + + "golang.org/x/crypto/sha3" + "golang.org/x/net/context" + "gopkg.in/check.v1" + "gopkg.in/macaroon.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/osutil" + "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/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/snap/snaptest" + "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/testutil" +) + +type apiBaseSuite struct { + 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() +} + +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) ([]*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) Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error { + panic("Download not expected to be called") +} + +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) Assertion(*asserts.AssertionType, []string, *auth.UserState) (asserts.Assertion, error) { + panic("Assertion not expected to be called") +} + +func (s *apiBaseSuite) Sections(*auth.UserState) ([]string, error) { + panic("Sections not expected to be called") +} + +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) + + snapstate.CanAutoRefresh = nil +} + +func (s *apiBaseSuite) TearDownSuite(c *check.C) { + muxVars = nil + s.restoreRelease() +} + +var ( + rootPrivKey, _ = assertstest.GenerateKey(1024) + storePrivKey, _ = assertstest.GenerateKey(752) +) + +func (s *apiBaseSuite) SetUpTest(c *check.C) { + 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", rootPrivKey, storePrivKey) + s.trustedRestorer = sysdb.InjectTrusted(s.storeSigning.Trusted) + + assertstateRefreshSnapDeclarations = nil + snapstateCoreInfo = 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 + snapstateCoreInfo = snapstate.CoreInfo + 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) + + s.d = d + return d +} + +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 = "beta" + + 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\ndescription: description\nsummary: summary\napps:\n cmd:\n command: some.cmd\n cmd2:\n command: other.cmd\n") + df := s.mkInstalledDesktopFile(c, "foo_cmd.desktop", "[Desktop]\nExec=foo.cmd %U") + + 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, map[string]interface{}{}) + m := rsp.Result.(map[string]interface{}) + + // installed-size depends on vagaries of the filesystem, just check type + c.Check(m["installed-size"], check.FitsTypeOf, int64(0)) + delete(m, "installed-size") + // ditto install-date + c.Check(m["install-date"], check.FitsTypeOf, time.Time{}) + delete(m, "install-date") + + meta := &Meta{} + expected := &resp{ + Type: ResponseTypeSync, + Status: 200, + Result: map[string]interface{}{ + "id": "foo-id", + "name": "foo", + "revision": snap.R(10), + "version": "v1", + "channel": "stable", + "tracking-channel": "beta", + "title": "title", + "summary": "summary", + "description": "description", + "developer": "bar", + "status": "active", + "icon": "/v2/icons/foo/icon", + "type": string(snap.TypeApp), + "resource": "/v2/snaps/foo", + "private": false, + "devmode": false, + "jailmode": false, + "confinement": snap.StrictConfinement, + "trymode": false, + "apps": []appJSON{ + {Name: "cmd", DesktopFile: df}, + // no desktop file + {Name: "cmd2"}, + }, + "broken": "", + "contact": "", + }, + 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", + "defaultCoreSnapName", + "oldDefaultSnapCoreName", + "errDevJailModeConflict", + "errNoJailMode", + "errClassicDevmodeConflict", + // snapInstruction vars: + "snapInstructionDispTable", + "snapstateInstall", + "snapstateUpdate", + "snapstateInstallPath", + "snapstateTryPath", + "snapstateCoreInfo", + "snapstateUpdateMany", + "snapstateInstallMany", + "snapstateRemoveMany", + "snapstateRefreshCandidates", + "snapstateRevert", + "snapstateRevertToRevision", + "assertstateRefreshSnapDeclarations", + "unsafeReadSnapInfo", + "osutilAddUser", + "setupLocalUser", + "storeUserInfo", + "postCreateUserUcrednetGetUID", + "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": "", + }, + "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, testutil.Contains, "cannot authenticate to snap store") +} + +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.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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() + 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"}) + st.Unlock() + + c.Check(soon, check.Equals, 1) +} + +func (s *apiSuite) TestPostSnapVerfySnapInstruction(c *check.C) { + d := s.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, "") + st.Unlock() +} + +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}, + {"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) TestPostSnapEnableDisableRevision(c *check.C) { + for _, action := range []string{"enable", "disable"} { + 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}, false) + 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, false) + 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, true) + 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, true) + 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" + d := s.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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" + d := s.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + // 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--") + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + return nil, nil + } + 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" + d := s.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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 + } + + for _, t := range []struct { + coreInfoErr error + nTasks int + installSnap string + flags snapstate.Flags + desc string + }{ + // core installed + {nil, 1, "", snapstate.Flags{}, "core; -"}, + {nil, 1, "", snapstate.Flags{DevMode: true}, "core; devmode"}, + {nil, 1, "", snapstate.Flags{JailMode: true}, "core; jailmode"}, + {nil, 1, "", snapstate.Flags{Classic: true}, "core; classic"}, + // no-core-installed + {state.ErrNoState, 2, "core", snapstate.Flags{}, "no core; -"}, + {state.ErrNoState, 2, "core", snapstate.Flags{DevMode: true}, "no core; devmode"}, + {state.ErrNoState, 2, "core", snapstate.Flags{JailMode: true}, "no core; jailmode"}, + {state.ErrNoState, 2, "core", snapstate.Flags{Classic: true}, "no 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 + } + + installSnap := "" + 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)) + } + installSnap = name + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + return nil, t.coreInfoErr + } + + // try the snap (without an installed core) + rsp := postSnaps(snapsCmd, reqForFlags(t.flags), nil).(*resp) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync, check.Commentf(t.desc)) + c.Assert(tryWasCalled, check.Equals, true, check.Commentf(t.desc)) + + st := d.overlord.State() + st.Lock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil, check.Commentf(t.desc)) + + c.Assert(chg.Tasks(), check.HasLen, t.nTasks, check.Commentf(t.desc)) + c.Check(installSnap, check.Equals, t.installSnap, check.Commentf(t.desc)) + + st.Unlock() + <-chg.Ready() + 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)) + st.Unlock() + } +} + +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, hasCoreSnap bool) string { + d := s.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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 + } + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + if hasCoreSnap { + return nil, nil + } + // pretend we do not have a state for ubuntu-core + return nil, state.ErrNoState + } + 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 + if !hasCoreSnap { + n++ + } + c.Assert(installQueue, check.HasLen, n) + if !hasCoreSnap { + c.Check(installQueue[0], check.Equals, defaultCoreSnapName) + } + 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() + <-chg.Ready() + 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) TestGetConfNoKey(c *check.C) { + result := s.runGetConf(c, nil, 400) + c.Check(result, check.DeepEquals, map[string]interface{}{"message": "cannot obtain configuration: no keys supplied"}) +} + +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) TestSetConfBadSnap(c *check.C) { + d := s.daemon(c) + + // 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, 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": "no state entry for key", + "kind": "snap-not-found"}, + "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() + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have core + return nil, nil + } + 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() { + snapstateCoreInfo = nil + snapstateInstall = nil + }() + + d := s.daemon(c) + + d.overlord.Loop() + defer d.overlord.Stop() + + 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() + <-chg.Ready() + 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 + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have core + return nil, nil + } + 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{} + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have core + return nil, nil + } + 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 + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have ubuntu-core + return nil, nil + } + 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{} + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have ubuntu-core + return nil, nil + } + 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.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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) TestInstallMissingCoreSnap(c *check.C) { + installQueue := []*state.Task{} + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // pretend we do not have a core + return nil, state.ErrNoState + } + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + t1 := s.NewTask("fake-install-snap", name) + t2 := s.NewTask("fake-install-snap", "second task is just here so that we can check that the wait is correctly added to all tasks") + installQueue = append(installQueue, t1, t2) + return state.NewTaskSet(t1, t2), nil + } + + d := s.daemon(c) + + d.overlord.Loop() + defer d.overlord.Stop() + + buf := bytes.NewBufferString(`{"action": "install"}`) + 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, 4) + + c.Check(installQueue, check.HasLen, 4) + // the two OS snap install tasks + c.Check(installQueue[0].Summary(), check.Equals, defaultCoreSnapName) + c.Check(installQueue[0].WaitTasks(), check.HasLen, 0) + c.Check(installQueue[1].WaitTasks(), check.HasLen, 0) + // the two "some-snap" install tasks + c.Check(installQueue[2].Summary(), check.Equals, "some-snap") + c.Check(installQueue[2].WaitTasks(), check.HasLen, 2) + c.Check(installQueue[3].WaitTasks(), check.HasLen, 2) +} + +// Installing ubuntu-core when not having ubuntu-core doesn't misbehave and try +// to install ubuntu-core twice. +func (s *apiSuite) TestInstallCoreSnapWhenMissing(c *check.C) { + installQueue := []*state.Task{} + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // pretend we do not have a core + return nil, state.ErrNoState + } + snapstateInstall = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + t1 := s.NewTask("fake-install-snap", name) + t2 := s.NewTask("fake-install-snap", "second task is just here so that we can check that the wait is correctly added to all tasks") + installQueue = append(installQueue, t1, t2) + return state.NewTaskSet(t1, t2), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + Snaps: []string{defaultCoreSnapName}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(installQueue, check.HasLen, 2) + // the only OS snap install tasks + c.Check(installQueue[0].Summary(), check.Equals, defaultCoreSnapName) + c.Check(installQueue[0].WaitTasks(), check.HasLen, 0) + c.Check(installQueue[1].WaitTasks(), check.HasLen, 0) +} + +func (s *apiSuite) TestInstallFails(c *check.C) { + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + // we have core + return nil, nil + } + + 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.daemon(c) + + d.overlord.Loop() + defer d.overlord.Stop() + + 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() + <-chg.Ready() + 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 + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + return nil, nil + } + + 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 + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + return nil, nil + } + + 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) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + 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", + }) +} + +// Test for POST /v2/interfaces + +func (s *apiSuite) TestConnectPlugSuccess(c *check.C) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + 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() + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, check.HasLen, 1) + c.Assert(slot.Connections, check.HasLen, 1) + c.Check(plug.Connections[0], check.DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"}) + c.Check(slot.Connections[0], check.DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"}) +} + +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) + + 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, 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() + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, check.HasLen, 0) + c.Assert(slot.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) + + d.overlord.Loop() + defer d.overlord.Stop() + + 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() + slot := repo.Slot("producer", "slot") + c.Assert(slot.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 + + d.overlord.Loop() + defer d.overlord.Stop() + + 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() + plug := repo.Plug("consumer", "plug") + c.Assert(plug.Connections, check.HasLen, 0) +} + +func (s *apiSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + 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) + + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, check.HasLen, 0) + c.Assert(slot.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) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + // there is no consumer, no plug defined + s.mockSnap(c, producerYaml) + + d.overlord.Loop() + defer d.overlord.Stop() + + 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) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + // there is no producer, no slot defined + + d.overlord.Loop() + defer d.overlord.Stop() + + 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) { + d := s.daemon(c) + + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + d.overlord.Loop() + defer d.overlord.Stop() + + 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 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, "3") + 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) + + _, err = dec.Decode() + c.Assert(err, check.Equals, io.EOF) + + ids := []string{a1.(*asserts.Account).AccountID(), a2.(*asserts.Account).AccountID(), a3.(*asserts.Account).AccountID()} + sort.Strings(ids) + c.Check(ids, check.DeepEquals, []string{"can0nical", "canonical", "developer1-id"}) +} + +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) + postCreateUserUcrednetGetUID = func(string) (uint32, error) { + return 0, nil + } + s.mockUserHome = c.MkDir() + userLookup = mkUserLookup(s.mockUserHome) +} + +func (s *postCreateUserSuite) TearDownTest(c *check.C) { + s.apiBaseSuite.TearDownTest(c) + + postCreateUserUcrednetGetUID = ucrednetGetUID + 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", + }) + 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}) + + postCreateUserUcrednetGetUID = func(string) (uint32, error) { + return 0, nil + } + defer func() { + postCreateUserUcrednetGetUID = ucrednetGetUID + }() + + // 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" }, `cannot find snap "lalala"`}, + {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 + + snapstateCoreInfo = func(s *state.State) (*snap.Info, error) { + return nil, nil + } + + 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) { + d := s.daemon(c) + d.overlord.Loop() + defer d.overlord.Stop() + + 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") +} diff --git a/daemon/daemon.go b/daemon/daemon.go new file mode 100644 index 00000000..7219a80b --- /dev/null +++ b/daemon/daemon.go @@ -0,0 +1,430 @@ +// -*- 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" + "strings" + "sync" + unix "syscall" + "time" + + "github.com/coreos/go-systemd/activation" + "github.com/gorilla/mux" + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/i18n/dumb" + "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" +) + +// 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 + + d *Daemon +} + +func (c *Command) canAccess(r *http.Request, user *auth.UserState) bool { + if user != nil { + // Authenticated users do anything for now. + return true + } + + isUser := false + uid, err := ucrednetGetUID(r.RemoteAddr) + if err == nil { + if uid == 0 { + // Superuser does anything. + return true + } + + isUser = true + } else if err != errNoUID { + logger.Noticef("unexpected error when attempting to get UID: %s", err) + return false + } else if c.SnapOK { + return true + } + + if r.Method != "GET" { + return false + } + + if isUser && c.UserOK { + return true + } + + if c.GuestOK { + return true + } + + return false +} + +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() + + if !c.canAccess(r, user) { + Unauthorized("access denied").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 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..675ce844 --- /dev/null +++ b/daemon/daemon_test.go @@ -0,0 +1,443 @@ +// -*- 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" + + "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/dirs" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { check.TestingT(t) } + +type daemonSuite struct{} + +var _ = check.Suite(&daemonSuite{}) + +func (s *daemonSuite) SetUpSuite(c *check.C) { + snapstate.CanAutoRefresh = nil +} + +func (s *daemonSuite) SetUpTest(c *check.C) { + dirs.SetRootDir(c.MkDir()) + err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) + c.Assert(err, check.IsNil) +} + +func (s *daemonSuite) TearDownTest(c *check.C) { + dirs.SetRootDir("") +} + +// 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 = "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 = "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, false) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + c.Check(cmd.canAccess(pst, nil), check.Equals, false) + c.Check(cmd.canAccess(del, nil), check.Equals, false) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, false) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + c.Check(cmd.canAccess(pst, nil), check.Equals, false) + c.Check(cmd.canAccess(del, nil), check.Equals, false) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + c.Check(cmd.canAccess(pst, nil), check.Equals, false) + c.Check(cmd.canAccess(del, nil), check.Equals, false) + + // 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, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) + c.Check(cmd.canAccess(pst, nil), check.Equals, true) + c.Check(cmd.canAccess(del, nil), check.Equals, true) +} + +func (s *daemonSuite) TestUserAccess(c *check.C) { + get := &http.Request{Method: "GET", RemoteAddr: "uid=42;"} + put := &http.Request{Method: "PUT", RemoteAddr: "uid=42;"} + + cmd := &Command{d: newTestDaemon(c)} + c.Check(cmd.canAccess(get, nil), check.Equals, false) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, false) + + // 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, false) + c.Check(cmd.canAccess(put, nil), check.Equals, false) +} + +func (s *daemonSuite) TestSuperAccess(c *check.C) { + get := &http.Request{Method: "GET", RemoteAddr: "uid=0;"} + put := &http.Request{Method: "PUT", RemoteAddr: "uid=0;"} + + cmd := &Command{d: newTestDaemon(c)} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) + + cmd = &Command{d: newTestDaemon(c), UserOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) + + cmd = &Command{d: newTestDaemon(c), GuestOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) + + cmd = &Command{d: newTestDaemon(c), SnapOK: true} + c.Check(cmd.canAccess(get, nil), check.Equals, true) + c.Check(cmd.canAccess(put, nil), check.Equals, true) +} + +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) TestStartStop(c *check.C) { + d := newTestDaemon(c) + st := d.overlord.State() + // mark as already seeded + st.Lock() + st.Set("seeded", true) + st.Unlock() + + 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 + st := d.overlord.State() + st.Lock() + st.Set("seeded", true) + st.Unlock() + + 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 + }) + + st := d.overlord.State() + // mark as already seeded + st.Lock() + st.Set("seeded", true) + st.Unlock() + + 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..cf89690b --- /dev/null +++ b/daemon/response.go @@ -0,0 +1,262 @@ +// -*- 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 ( + "encoding/json" + "fmt" + "mime" + "net/http" + "path/filepath" + "strconv" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/logger" +) + +// 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") + 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)) +} + +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) +) + +func SnapNotFound(err error) Response { + return &resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindSnapNotFound, + }, + Status: 404, + } +} diff --git a/daemon/response_test.go b/daemon/response_test.go new file mode 100644 index 00000000..9409fcf6 --- /dev/null +++ b/daemon/response_test.go @@ -0,0 +1,99 @@ +// -*- 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" + "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)) +} diff --git a/daemon/snap.go b/daemon/snap.go new file mode 100644 index 00000000..c6e24397 --- /dev/null +++ b/daemon/snap.go @@ -0,0 +1,294 @@ +// -*- 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" + "time" + + "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/snap" +) + +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 +} + +// appJSON contains the json for snap.AppInfo +type appJSON struct { + Name string `json:"name"` + Daemon string `json:"daemon"` + DesktopFile string `json:"desktop-file,omitempty"` +} + +// screenshotJSON contains the json for snap.ScreenshotInfo +type screenshotJSON struct { + URL string `json:"url"` + Width int64 `json:"width,omitempty"` + Height int64 `json:"height,omitempty"` +} + +func mapLocal(about aboutSnap) map[string]interface{} { + localSnap, snapst := about.info, about.snapst + status := "installed" + if snapst.Active && localSnap.Revision == snapst.Current { + status = "active" + } + + appNames := make([]string, 0, len(localSnap.Apps)) + for appName := range localSnap.Apps { + appNames = append(appNames, appName) + } + sort.Strings(appNames) + apps := make([]appJSON, 0, len(localSnap.Apps)) + for _, appName := range appNames { + app := localSnap.Apps[appName] + var installedDesktopFile string + if osutil.FileExists(app.DesktopFile()) { + installedDesktopFile = app.DesktopFile() + } + + apps = append(apps, appJSON{ + Name: app.Name, + Daemon: app.Daemon, + DesktopFile: installedDesktopFile, + }) + } + + // TODO: expose aliases information and state? + + result := map[string]interface{}{ + "description": localSnap.Description(), + "developer": about.publisher, + "icon": snapIcon(localSnap), + "id": localSnap.SnapID, + "install-date": snapDate(localSnap), + "installed-size": localSnap.Size, + "name": localSnap.Name(), + "revision": localSnap.Revision, + "status": status, + "summary": localSnap.Summary(), + "type": string(localSnap.Type), + "version": localSnap.Version, + "channel": localSnap.Channel, + "tracking-channel": snapst.Channel, + "confinement": localSnap.Confinement, + "devmode": snapst.DevMode, + "trymode": snapst.TryMode, + "jailmode": snapst.JailMode, + "private": localSnap.Private, + "apps": apps, + "broken": localSnap.Broken, + "contact": localSnap.Contact, + } + + if localSnap.Title() != "" { + result["title"] = localSnap.Title() + } + + return result +} + +func mapRemote(remoteSnap *snap.Info) map[string]interface{} { + status := "available" + if remoteSnap.MustBuy { + status = "priced" + } + + confinement := remoteSnap.Confinement + if confinement == "" { + confinement = snap.StrictConfinement + } + + screenshots := make([]screenshotJSON, len(remoteSnap.Screenshots)) + for i, screenshot := range remoteSnap.Screenshots { + screenshots[i] = screenshotJSON{ + URL: screenshot.URL, + Width: screenshot.Width, + Height: screenshot.Height, + } + } + + result := map[string]interface{}{ + "description": remoteSnap.Description(), + "developer": remoteSnap.Publisher, + "download-size": 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": confinement, + "contact": remoteSnap.Contact, + } + + if remoteSnap.Title() != "" { + result["title"] = remoteSnap.Title() + } + + if len(screenshots) > 0 { + result["screenshots"] = screenshots + } + + if len(remoteSnap.Prices) > 0 { + result["prices"] = remoteSnap.Prices + } + + if len(remoteSnap.Channels) > 0 { + result["channels"] = remoteSnap.Channels + } + + if len(remoteSnap.Tracks) > 0 { + result["tracks"] = remoteSnap.Tracks + } + + return result +} diff --git a/daemon/ucrednet.go b/daemon/ucrednet.go new file mode 100644 index 00000000..20853fa9 --- /dev/null +++ b/daemon/ucrednet.go @@ -0,0 +1,95 @@ +// -*- 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 errNoUID = errors.New("no uid found") + +const ucrednetNobody = uint32((1 << 32) - 1) + +func ucrednetGetUID(remoteAddr string) (uint32, error) { + idx := strings.IndexByte(remoteAddr, ';') + if !strings.HasPrefix(remoteAddr, "uid=") || idx < 5 { + return ucrednetNobody, errNoUID + } + + uid, err := strconv.ParseUint(remoteAddr[4:idx], 10, 32) + if err != nil { + return ucrednetNobody, err + } + + return uint32(uid), nil +} + +type ucrednetAddr struct { + net.Addr + uid string +} + +func (wa *ucrednetAddr) String() string { + return fmt.Sprintf("uid=%s;%s", wa.uid, wa.Addr) +} + +type ucrednetConn struct { + net.Conn + uid string +} + +func (wc *ucrednetConn) RemoteAddr() net.Addr { + return &ucrednetAddr{wc.Conn.RemoteAddr(), 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 + } + + uid := "" + 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 + } + + uid = strconv.FormatUint(uint64(ucred.Uid), 10) + } + + return &ucrednetConn{con, uid}, err +} diff --git a/daemon/ucrednet_test.go b/daemon/ucrednet_test.go new file mode 100644 index 00000000..e8bb5973 --- /dev/null +++ b/daemon/ucrednet_test.go @@ -0,0 +1,172 @@ +// -*- 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{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, "uid=42;.*") + uid, err := ucrednetGetUID(remoteAddr) + 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, "uid=;.*") + uid, err := ucrednetGetUID(remoteAddr) + c.Check(uid, check.Equals, ucrednetNobody) + c.Check(err, check.Equals, errNoUID) +} + +func (s *ucrednetSuite) TestAcceptErrors(c *check.C) { + s.ucred = &sys.Ucred{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) { + uid, err := ucrednetGetUID("uid=;") + c.Check(err, check.Equals, errNoUID) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetBadUid(c *check.C) { + uid, err := ucrednetGetUID("uid=hello;") + c.Check(err, check.NotNil) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetNonUcrednet(c *check.C) { + uid, err := ucrednetGetUID("hello") + c.Check(err, check.Equals, errNoUID) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGetNothing(c *check.C) { + uid, err := ucrednetGetUID("") + c.Check(err, check.Equals, errNoUID) + c.Check(uid, check.Equals, ucrednetNobody) +} + +func (s *ucrednetSuite) TestGet(c *check.C) { + uid, err := ucrednetGetUID("uid=42;") + c.Check(err, check.IsNil) + c.Check(uid, check.Equals, uint32(42)) +} diff --git a/data/completion/complete.sh b/data/completion/complete.sh new file mode 100644 index 00000000..6cbad3ec --- /dev/null +++ b/data/completion/complete.sh @@ -0,0 +1,116 @@ +# -*- 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 +_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 + ) + +} + +# _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). +_complete_from_snap_maybe() { + # catch /snap/bin and /var/lib/snapd/snap/bin + if [[ "$(which "$1")" =~ /snap/bin/ && ( -e /var/lib/snapd/snap/core/current/usr/lib/snapd/etelpmoc.sh || -e /snap/core/current/usr/lib/snapd/etelpmoc.sh ) ]]; then + _complete_from_snap "$1" + return $? + fi + # fallback to the old -D + _completion_loader "$1" +} + +complete -D -F _complete_from_snap_maybe 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-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..71aa3bb4 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service/bin/start @@ -0,0 +1,6 @@ +#!/bin/sh + +while true; do + echo "running" + sleep 10 +done 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..1bd97ca1 --- /dev/null +++ b/tests/lib/snaps/test-snapd-service/meta/snap.yaml @@ -0,0 +1,8 @@ +name: test-snapd-service +version: 1.0 +apps: + test-snapd-service: + command: bin/start + daemon: simple + reload-command: bin/reload + 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..3ac75f9d --- /dev/null +++ b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml @@ -0,0 +1,6 @@ +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 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-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/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..05910b2d --- /dev/null +++ b/tests/lib/store.sh @@ -0,0 +1,68 @@ +#!/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..31758a62 --- /dev/null +++ b/tests/lib/systemd.sh @@ -0,0 +1,20 @@ +#!/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 status "$1"; then + systemctl stop "$1" + fi + rm -f "/run/systemd/system/$1.service" + systemctl daemon-reload +} diff --git a/tests/main/abort/task.yaml b/tests/main/abort/task.yaml new file mode 100644 index 00000000..7eff24af --- /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="$SNAPMOUNTDIR/$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..1653bff7 --- /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 "$SNAPMOUNTDIR/bin/alias1" + test -h "$SNAPMOUNTDIR/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 "$SNAPMOUNTDIR/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 "$SNAPMOUNTDIR/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 "$SNAPMOUNTDIR/bin/alias1" + test ! -e "$SNAPMOUNTDIR/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..7cc5b702 --- /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 "$SNAPMOUNTDIR/bin/test_snapd_wellknown1" + test -h "$SNAPMOUNTDIR/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 "$SNAPMOUNTDIR/bin/test_snapd_wellknown1" + test ! -e "$SNAPMOUNTDIR/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 "$SNAPMOUNTDIR/bin/test_snapd_wellknown1" + test ! -e "$SNAPMOUNTDIR/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 "$SNAPMOUNTDIR/bin/test_snapd_wellknown1" + test -h "$SNAPMOUNTDIR/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..e5acc96d --- /dev/null +++ b/tests/main/auto-refresh/task.yaml @@ -0,0 +1,53 @@ +summary: Check that auto-refresh works + +prepare: | + snap install --devmode jq + +restore: | + . "$TESTSLIB/dirs.sh" + if [ -e "$SNAPMOUNTDIR/core/current/meta/hooks/configure" ]; then + snap set core refresh.schedule="$(date +%a --date=2days)@12:00-14:00" + snap set core refresh.disabled=true + fi + +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 "$SNAPMOUNTDIR/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/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/task.yaml b/tests/main/classic-confinement/task.yaml new file mode 100644 index 00000000..a0022819 --- /dev/null +++ b/tests/main/classic-confinement/task.yaml @@ -0,0 +1,47 @@ +summary: Ensure that classic confinement works + +environment: + CLASSIC_SNAP: test-snapd-classic-confinement + +prepare: | + . "$TESTSLIB/snaps.sh" + snapbuild "$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 + + if [[ "$SPREAD_SYSTEM" =~ core ]]; then + echo "Check that the classic snap is not installable even with --classic" + ( snap install --dangerous --classic "${CLASSIC_SNAP}_1.0_all.snap" 2>&1 && exit 1 || true ) | MATCH $'cannot install snap file: classic confinement is only supported on classic\n *systems' + echo "Not from the store either" + ( snap install --classic "$CLASSIC_SNAP" 2>&1 && exit 1 || true ) | MATCH "requires classic confinement" + + exit 0 + 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..c97d177a --- /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" + + snapbuild "$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..e9cf6b2d --- /dev/null +++ b/tests/main/classic-ubuntu-core-transition-auth/task.yaml @@ -0,0 +1,38 @@ +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 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] + +warn-timeout: 1m +kill-timeout: 5m +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" + snap debug ensure-state-soon + + while ! snap changes|grep ".*Done.*Transition ubuntu-core to core"; do + snap changes + sleep 1 + done + + 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..bac20683 --- /dev/null +++ b/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml @@ -0,0 +1,52 @@ +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 +execute: | + . "$TESTSLIB/pkgdb.sh" + echo "Ensure we have two cores" + distro_install_package jq + + 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" + snap debug ensure-state-soon + + . $TESTSLIB/changes.sh + while ! snap changes|grep ".*Done.*Transition ubuntu-core to core"; do + snap changes + snap change $(change_id "Transition ubuntu-core to core")||true + sleep 1 + done + + 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..332af1aa --- /dev/null +++ b/tests/main/classic-ubuntu-core-transition/task.yaml @@ -0,0 +1,100 @@ +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 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] + +warn-timeout: 1m +kill-timeout: 5m + +restore: | + rm -f state.json.new + +execute: | + . "$TESTSLIB/pkgdb.sh" + + wait_for_service() { + local service_name="$1" + local state="${2:-active}" + while ! systemctl show -p ActiveState "$service_name" | grep -q "ActiveState=$state"; do + systemctl status "$service_name" || true; sleep 1; + done + } + 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" + snap debug ensure-state-soon + + . "$TESTSLIB/changes.sh" + while ! snap changes|grep ".*Done.*Transition ubuntu-core to core"; do + snap changes + snap change "$(change_id 'Transition ubuntu-core to core')" || true + sleep 1 + done + + 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/buy.exp b/tests/main/completion/buy.exp new file mode 100644 index 00000000..5c9cd025 --- /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 "test-\t\t" "test-assumes*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..fe688d57 --- /dev/null +++ b/tests/main/completion/info.exp @@ -0,0 +1,10 @@ +source lib.exp0 + +# info completes directories and snap files, and locally installed snaps +chat "snap inf\t\t\t" "bar.snap*core*testdir/" + +# with 3+ chars, also remote snaps +chat "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..eaf6f8bb --- /dev/null +++ b/tests/main/completion/install.exp @@ -0,0 +1,21 @@ +source lib.exp0 + +# install completes directories and snaps +chat "snap ins\t\t\t" "bar.snap*testdir/" + +# install with 3+ chars also completes store stuff +chat "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..969eabaf --- /dev/null +++ b/tests/main/completion/task.yaml @@ -0,0 +1,28 @@ +summary: Check different completions + +systems: + - -ubuntu-core-16-* + # ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 + - -ubuntu-*-ppc64el + +prepare: | + mkdir -p testdir + touch testdir/foo.snap + touch bar.snap + snap install core + snap install test-snapd-tools + "$TESTSLIB"/mkpinentry.sh + expect -d -f key.exp0 + +restore: | + rm testdir/foo.snap bar.snap + rmdir testdir + +debug: | + sysctl kernel.random.entropy_avail || true + +execute: | + for i in *.exp; do + echo $i + expect -d -f $i + done diff --git a/tests/main/completion/toplevel.exp b/tests/main/completion/toplevel.exp new file mode 100644 index 00000000..449e8f38 --- /dev/null +++ b/tests/main/completion/toplevel.exp @@ -0,0 +1,23 @@ +source lib.exp0 + +# --help completes +chat "snap --h\t" "snap --help $" true +# but no more +chat "\t\t" "snap --help $" +cancel + +# --version completes +chat "snap --v\t" "snap --version $" true +# but no more +chat "\t\t" "snap --version $" +cancel + +# commands complete +rechat "snap in\t\t" "\[\n ]info\[\r ]" true +rechat "" "\[\n ]install\[\r ]" true +rechat "" "\[\n ]interfaces\[\r ]" + +chat "s\t" "snap install $" + +cancel +brexit diff --git a/tests/main/completion/try.exp b/tests/main/completion/try.exp new file mode 100644 index 00000000..b0d34d2e --- /dev/null +++ b/tests/main/completion/try.exp @@ -0,0 +1,6 @@ +source lib.exp0 + +chat "snap try \t" "testdir/" + +cancel +brexit diff --git a/tests/main/completion/watch.exp b/tests/main/completion/watch.exp new file mode 100644 index 00000000..51cfe369 --- /dev/null +++ b/tests/main/completion/watch.exp @@ -0,0 +1,7 @@ +source lib.exp0 + +# watch completes with change ids +rechat "snap watch \t\t" "1 *2" + +cancel +brexit diff --git a/tests/main/config-versions/task.yaml b/tests/main/config-versions/task.yaml new file mode 100644 index 00000000..06048157 --- /dev/null +++ b/tests/main/config-versions/task.yaml @@ -0,0 +1,64 @@ +summary: Check that snap configuration is maintained on per-revision basis. + +systems: [-ubuntu-core-16-*] + +details: | + This test ensures that snap configuration is mainted per snap revision + and is restored on revert. + +prepare: | + snapbuild $TESTSLIB/snaps/config-versions . + snapbuild $TESTSLIB/snaps/config-versions-v2 . + +restore: | + rm config-versions_1.0_all.snap + rm config-versions_2.0_all.snap + +execute: | + verify_config_value() { + expected="$1" + output=$(snap get config-versions value) + if [ "$output" != "$expected" ]; then + echo "Expected config value to be $expected but got $output" + exit 1 + fi + } + + echo "Install test snap" + snap install --dangerous config-versions_1.0_all.snap + + echo "Setting config value affecting rev 1" + snap set config-versions value=100 + + echo "Install a new version of the test snap" + snap install --dangerous config-versions_2.0_all.snap + + echo "Expecting config value to be carried over to the new version 2" + verify_config_value 100 + + echo "Changing the config value affecting rev 2" + snap set config-versions value=200 + verify_config_value 200 + + echo "Refreshing to rev 1 should not restore config" + snap refresh --revision=x1 config-versions + verify_config_value 200 + + echo "Changing the config value affecting rev 1" + snap set config-versions value=300 + + echo "Refreshing back to the rev 2 should not restore config" + snap refresh --revision=x2 config-versions + verify_config_value 300 + + echo "Changing the config value affecting rev 2" + snap set config-versions value=400 + verify_config_value 400 + + echo "Reverting to the rev 1" + snap revert config-versions + verify_config_value 300 + + echo "Revert back to the rev 2" + snap revert --revision=x2 config-versions + verify_config_value 400 diff --git a/tests/main/confinement-classic/task.yaml b/tests/main/confinement-classic/task.yaml new file mode 100644 index 00000000..910e6887 --- /dev/null +++ b/tests/main/confinement-classic/task.yaml @@ -0,0 +1,29 @@ +summary: trivial snap with classic confinement runs correctly +details: | + This test checks that a very much trivial "hello-world"-like snap using + classic confinement can be executed correctly. There are two variants of + this test (classic and jailmode) and the snap (this particular one) should + function correctly in both cases. +systems: [-ubuntu-core-16-*] +execute: | + . $TESTSLIB/dirs.sh + + run_install() { + make -C test-snapd-hello-classic clean + make -C test-snapd-hello-classic + snap install "$@" --dangerous ./test-snapd-hello-classic/test-snapd-hello-classic_0.1_*.snap + } + + run_install --classic + $SNAPMOUNTDIR/bin/test-snapd-hello-classic | MATCH 'Hello Classic!' + + if [ "$(snap debug confinement)" = partial ]; then + exit 0 + fi + + # Installing again will increase revision and put the snap into jailmode + run_install --classic --jailmode + $SNAPMOUNTDIR/bin/test-snapd-hello-classic | MATCH 'Hello Classic!' + +restore: | + make -C test-snapd-hello-classic clean diff --git a/tests/main/confinement-classic/test-snapd-hello-classic/Makefile b/tests/main/confinement-classic/test-snapd-hello-classic/Makefile new file mode 100644 index 00000000..9dc50f02 --- /dev/null +++ b/tests/main/confinement-classic/test-snapd-hello-classic/Makefile @@ -0,0 +1,77 @@ +SNAP_NAME = test-snapd-hello-classic +SNAP_VERSION = 0.1 + +# Ask gcc about the architecture name +arch := $(shell $(CC) -dumpmachine) + +ifeq ($(arch),x86_64-linux-gnu) +snap_arch = amd64 +dynamic_linker=ld-linux-x86-64.so.2 +else ifeq ($(arch),i686-linux-gnu) +# NOTE: arch needs to be i386-linux-gnu, not i686-linux-gnu +arch := i386-linux-gnu +snap_arch = i386 +dynamic_linker=ld-linux.so.2 +else ifeq ($(arch),aarch64-linux-gnu) +snap_arch = arm64 +dynamic_linker=ld-linux-aarch64.so.1 +else ifeq ($(arch),arm-linux-gnueabihf) +snap_arch = armhf +dynamic_linker=ld-linux-armhf.so.3 +else ifeq ($(arch),powerpc64le-linux-gnu) +# NOTE: The architecture name is ppc64el (E-L) but GCC uses powerpc64le (L-E) +snap_arch = ppc64el +dynamic_linker=ld64.so.2 +else +$(error cannot translate architecture $(arch) to snap equivalent) +endif + +# Name of the snap we're building +snap_file = $(SNAP_NAME)_$(SNAP_VERSION)_$(snap_arch).snap + +define snap_yaml +name: $(SNAP_NAME) +version: $(SNAP_VERSION) +summary: A hello-world with classic confinement +architectures: [$(snap_arch)] +apps: + $(SNAP_NAME): + command: test-snapd-hello-classic.$(snap_arch).bin +confinement: classic +endef + +.PHONY: all +all: $(snap_file) + +# Name of the core snap to use +snap_core=core + +# Don't search in default locations +LDFLAGS += -Wl,-z,nodefaultlib +LDFLAGS += -Wl,--enable-new-dtags +# Search in the core snap +LDFLAGS += -Wl,-rpath,/snap/$(snap_core)/current/lib/$(arch):/snap/$(snap_core)/current/usr/lib/$(arch) +# Use the dynamic linker from the core snap +LDFLAGS += -Wl,--dynamic-linker=/snap/$(snap_core)/current/lib/$(arch)/$(dynamic_linker) + +test-snapd-hello-classic.$(snap_arch).bin: test-snapd-hello-classic.c + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< + +$(snap_file): test-snapd-hello-classic.$(snap_arch).bin meta/snap.yaml + mksquashfs . $@ -e $@ -noappend -no-xattrs -comp xz + +meta: Makefile + mkdir -p $@ + +export snap_yaml +meta/snap.yaml: Makefile | meta + echo "$$snap_yaml" > $@ + +.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..25d58ae0 --- /dev/null +++ b/tests/main/confinement-classic/test-snapd-hello-classic/test-snapd-hello-classic.c @@ -0,0 +1,7 @@ +#include + +int main() +{ + printf("Hello Classic!\n"); + return 0; +} 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..275a825f --- /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..4f20dea8 --- /dev/null +++ b/tests/main/create-user/task.yaml @@ -0,0 +1,30 @@ +summary: Ensure create-user functionality + +systems: [-ubuntu-core-16-*] + +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..64f9707e --- /dev/null +++ b/tests/main/debs-have-built-using/task.yaml @@ -0,0 +1,10 @@ +summary: Ensure that our debs have the "built-using" header +systems: [-ubuntu-core-*] +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..9fb345a0 --- /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 $SNAPMOUNTDIR/core/current ]; then + core_inode="$(stat -c '%i' $SNAPMOUNTDIR/core/current/$DIRECTORY)" + else + core_inode="$(stat -c '%i' $SNAPMOUNTDIR/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..d6f9437a --- /dev/null +++ b/tests/main/econnreset/task.yaml @@ -0,0 +1,41 @@ +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 + + 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 20); 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" + 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..a1152044 --- /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 $SNAPMOUNTDIR/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..1b959341 --- /dev/null +++ b/tests/main/failover/task.yaml @@ -0,0 +1,109 @@ +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 + +restore: | + rm -f failing.snap failBoot currentBoot prevBoot + rm -rf /tmp/unpack + + # 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 /tmp/unpack/etc/rc.local + cat < /tmp/unpack/etc/rc.local + #!bin/sh + printf c > /proc/sysrq-trigger + EOF + } + + inject_emptysystemd_failure(){ + truncate -s 0 /tmp/unpack/lib/systemd/systemd + } + + inject_emptyinitrd_failure(){ + truncate -s 0 /tmp/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 /tmp/unpack /var/lib/snapd/snaps/${TARGET_SNAP_NAME}_$(cat prevBoot).snap + + # set failure condition + eval ${INJECT_FAILURE} + + # repack new target snap + snapbuild /tmp/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/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/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..db2c0603 --- /dev/null +++ b/tests/main/i18n/task.yaml @@ -0,0 +1,16 @@ +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_DE.UTF-8 snap changes everything | MATCH "Ja, ja, allerdings." 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-hook/task.yaml b/tests/main/install-hook/task.yaml new file mode 100644 index 00000000..c390deff --- /dev/null +++ b/tests/main/install-hook/task.yaml @@ -0,0 +1,58 @@ +summary: Check install and remove 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 install hook is run only once" + snap set snap-hooks installed=2 + install_local snap-hooks + snap get snap-hooks installed | MATCH 2 + + 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..2ea9eac5 --- /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 + snapbuild $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 ./${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 $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/test-snapd-tools/x1 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..2cbe65c4 --- /dev/null +++ b/tests/main/install-store/task.yaml @@ -0,0 +1,38 @@ +summary: Checks for special cases of snap install from the store + +systems: [ubuntu-core-16-*] + +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" diff --git a/tests/main/interfaces-account-control/task.yaml b/tests/main/interfaces-account-control/task.yaml new file mode 100644 index 00000000..505cfa24 --- /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 + + $SNAPMOUNTDIR/bin/account-control-consumer.useradd --extrausers alice + echo alice:password | $SNAPMOUNTDIR/bin/account-control-consumer.chpasswd + + # User deletion is unsupported yet on Core: https://bugs.launchpad.net/ubuntu/+source/shadow/+bug/1659534 + # $SNAPMOUNTDIR/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..721f2e2c --- /dev/null +++ b/tests/main/interfaces-alsa/task.yaml @@ -0,0 +1,97 @@ +summary: Ensure that the alsa interface works. + +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-avahi-observe/task.yaml b/tests/main/interfaces-avahi-observe/task.yaml new file mode 100644 index 00000000..1f332d90 --- /dev/null +++ b/tests/main/interfaces-avahi-observe/task.yaml @@ -0,0 +1,52 @@ +summary: check that avahi-observe interface works + +systems: [-ubuntu-core-*] + +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 + cat avahi.error | MATCH "org.freedesktop.DBus.Error.AccessDenied" + 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-cli/task.yaml b/tests/main/interfaces-cli/task.yaml new file mode 100644 index 00000000..c4be362b --- /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" + snapbuild $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/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..1989b0b9 --- /dev/null +++ b/tests/main/interfaces-cups-control/task.yaml @@ -0,0 +1,78 @@ +summary: Ensure that the cups interface works. + +systems: + - -ubuntu-core-16-* + +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 + + echo "And the pdf printer is available" + . "$TESTSLIB/pkgdb.sh" + distro_install_package --no-install-recommends cups printer-driver-cups-pdf + + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + # Not all distributions are starting the cups service directly after + # the package was installed. + if ! systemctl is-enabled cups ; then + # We can't use --now as this isn't supported by all distributions + systemctl enable cups + systemctl start cups + fi + fi + +restore: | + . "$TESTSLIB/pkgdb.sh" + distro_purge_package cups printer-driver-cups-pdf + rm -rf $HOME/PDF $TEST_FILE print.error + +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/test_file*.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..490940bc --- /dev/null +++ b/tests/main/interfaces-dbus/task.yaml @@ -0,0 +1,91 @@ +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" + . "$TESTSLIB/pkgdb.sh" + + echo "Give a snap declaring a dbus slot in installed" + snap install --edge test-snapd-dbus-provider + + echo "And the D-bus X11 dependencies are installed" + distro_install_package dbus-x11 + + 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" + dbus-launch > dbus.env + export $(cat dbus.env | xargs) + if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then + cat < /etc/init/dbus-provider.conf + env DISPLAY="$DISPLAY" + env DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" + env DBUS_SESSION_BUS_PID="$DBUS_SESSION_BUS_PID" + script + $SNAPMOUNTDIR/bin/test-snapd-dbus-provider.provider + end script + EOF + initctl reload-configuration + start dbus-provider + else + systemd-run --unit dbus-provider \ + --setenv=DISPLAY=$DISPLAY \ + --setenv=DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS \ + --setenv=DBUS_SESSION_BUS_PID=$DBUS_SESSION_BUS_PID \ + $SNAPMOUNTDIR/bin/test-snapd-dbus-provider.provider + fi + +restore: | + . "$TESTSLIB/pkgdb.sh" + rm -f call.error dbus.env + distro_purge_package dbus-x11 + if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then + stop dbus-provider + rm -f /etc/init/dbus-provider.conf + else + systemctl stop dbus-provider + fi + +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 | xargs) + + 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-firewall-control/task.yaml b/tests/main/interfaces-firewall-control/task.yaml new file mode 100644 index 00000000..c98833d7 --- /dev/null +++ b/tests/main/interfaces-firewall-control/task.yaml @@ -0,0 +1,87 @@ +summary: Ensure that the firewall-control interface works. + +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" + snapbuild $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 -q 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-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..2561c5fb --- /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" + snapbuild $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-home/task.yaml b/tests/main/interfaces-home/task.yaml new file mode 100644 index 00000000..cf044e22 --- /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" + snapbuild $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..090d20c1 --- /dev/null +++ b/tests/main/interfaces-hooks/task.yaml @@ -0,0 +1,21 @@ +summary: Check that `snap connect` runs interface hook + +prepare: | + echo "Build test hooks package" + snapbuild $TESTSLIB/snaps/basic-iface-hooks-consumer . + snapbuild $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 basic-iface-hooks-consumer_1.0_all.snap + rm basic-iface-hooks-producer_1.0_all.snap + +restore: | + rm basic-iface-hooks-consumer_1.0_all.snap + rm 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..e8d638c7 --- /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 /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 "`$SNAPMOUNTDIR/bin/iio-consumer.read`" = "iio-0" + + $SNAPMOUNTDIR/bin/iio-consumer.write "hello" + test "`$SNAPMOUNTDIR/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..dca7cbd0 --- /dev/null +++ b/tests/main/interfaces-kernel-module-control/task.yaml @@ -0,0 +1,129 @@ +summary: Ensure that the kernel-module-control interface works. + +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 binfmt_misc 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 binfmt_misc && ! -f module_present; then + rmmod binfmt_misc + elif [ -f module_present ]; then + insmod /lib/modules/$(uname -r)/kernel/fs/binfmt_misc.ko + fi + if [ -f package_present ]; then + apt install -y binfmt-support + fi + rm -f module_present package_present + +debug: | + lsmod + +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 binfmt_misc; then + touch module_present + if ! rmmod binfmt_misc; then + # the module is being used + if apt list --installed binfmt-support | MATCH binfmt-support; then + touch package_present + apt remove -y binfmt-support + rmmod binfmt_misc + fi + fi + fi + lsmod | MATCH -v binfmt_misc + test-snapd-kernel-module-consumer.insmod /lib/modules/$(uname -r)/kernel/fs/binfmt_misc.ko + + echo "And the snap is able to read /sys/module" + generic-consumer.cmd ls /sys/module | MATCH binfmt_misc + + 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 binfmt_misc + lsmod | MATCH -v binfmt_misc + + 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 /lib/modules/$(uname -r)/kernel/fs/binfmt_misc.ko; 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 binfmt_misc + insmod /lib/modules/$(uname -r)/kernel/fs/binfmt_misc.ko + lsmod | MATCH binfmt_misc + if test-snapd-kernel-module-consumer.rmmod binfmt_misc 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..4fc24122 --- /dev/null +++ b/tests/main/interfaces-libvirt/task.yaml @@ -0,0 +1,82 @@ +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: | + echo "Given libvirt and qemu are installed" + apt install -y libvirt-bin qemu + # add test user 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: | + apt autoremove -y --purge libvirt-bin qemu + + 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..b0e83590 --- /dev/null +++ b/tests/main/interfaces-locale-control/task.yaml @@ -0,0 +1,104 @@ +summary: Ensure that the locale-control interface works. + +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. + +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" + snapbuild $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..0f7376a6 --- /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" + snapbuild $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-mount-observe/task.yaml b/tests/main/interfaces-mount-observe/task.yaml new file mode 100644 index 00000000..4e01ba3c --- /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" + snapbuild $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="$SNAPMOUNTDIR/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="$SNAPMOUNTDIR/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..a039c58e --- /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" + snapbuild $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/task.yaml b/tests/main/interfaces-network-control/task.yaml new file mode 100644 index 00000000..32c59bba --- /dev/null +++ b/tests/main/interfaces-network-control/task.yaml @@ -0,0 +1,157 @@ +summary: Ensure that the network-control interface works. + +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" + snapbuild $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 -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" + . "$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-observe/task.yaml b/tests/main/interfaces-network-observe/task.yaml new file mode 100644 index 00000000..4ebf7a18 --- /dev/null +++ b/tests/main/interfaces-network-observe/task.yaml @@ -0,0 +1,66 @@ +summary: Ensure that the network-observe interface works + +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" + snapbuild $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 -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 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/task.yaml b/tests/main/interfaces-network/task.yaml new file mode 100644 index 00000000..9fbd5879 --- /dev/null +++ b/tests/main/interfaces-network/task.yaml @@ -0,0 +1,77 @@ +summary: Ensure network interface works. + +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" + snapbuild $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..14b18c47 --- /dev/null +++ b/tests/main/interfaces-password-manager-service/task.yaml @@ -0,0 +1,54 @@ +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" + distro_install_package gnome-keyring dbus-x11 + snap install --edge test-snapd-password-manager-consumer + +restore: | + . $TESTSLIB/pkgdb.sh + distro_auto_remove_packages gnome-keyring dbus-x11 + 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-process-control/task.yaml b/tests/main/interfaces-process-control/task.yaml new file mode 100644 index 00000000..a62ed9b2 --- /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" + snapbuild $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/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..f888f442 --- /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 "`$SNAPMOUNTDIR/bin/time-control-consumer.read`" + $SNAPMOUNTDIR/bin/time-control-consumer.write + + # Read/write access should be possible + test -n "`$SNAPMOUNTDIR/bin/time-control-consumer.timedatectl status`" + $SNAPMOUNTDIR/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..9148ac4f --- /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" + snapbuild $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-upower-observe/task.yaml b/tests/main/interfaces-upower-observe/task.yaml new file mode 100644 index 00000000..3ec4103e --- /dev/null +++ b/tests/main/interfaces-upower-observe/task.yaml @@ -0,0 +1,62 @@ +summary: Ensure that the upower-observe interface works. + +systems: + - -ubuntu-core-16-* + # ppc64el disabled because of https://github.com/snapcore/snapd/issues/2504 + - -ubuntu-*-ppc64el + +summary: | + 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 + + . "$TESTSLIB/pkgdb.sh" + distro_install_package upower + +restore: | + rm -f upower.error + . "$TESTSLIB/pkgdb.sh" + distro_purge_package upower + +execute: | + CONNECTED_PATTERN=":upower-observe +test-snapd-upower-observe-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-upower-observe-consumer:upower-observe" + + echo "Then it is connected by default" + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "===================================" + + echo "When the plug is disconnected" + snap disconnect test-snapd-upower-observe-consumer:upower-observe + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "Then the snap is not able to dump info about the upower devices" + if su -l -c "test-snapd-upower-observe-consumer.upower --dump 2>${PWD}/upower.error" test; then + echo "Expected permission error accessing upower info with disconnected plug" + exit 1 + fi + grep -q "Permission denied" upower.error + + echo "===================================" + fi + + echo "When the plug is connected" + snap connect test-snapd-upower-observe-consumer:upower-observe + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to dump info about the upower devices" + expected="(?s)Device: +/org/freedesktop/UPower/devices/DisplayDevice.*Daemon:.*" + # debug + su -l -c 'test-snapd-upower-observe-consumer.upower --dump' test || true + su -l -c 'test-snapd-upower-observe-consumer.upower --dump' test | grep -Pqz "$expected" 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..ec3ed3de --- /dev/null +++ b/tests/main/listing/task.yaml @@ -0,0 +1,38 @@ +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 ubuntu-core snap is sideloaded" + expected='^core .* [0-9]{2}-[0-9.]+(\+git[0-9]+\.[0-9a-f]+)? +x[0-9]+ +core *$' + else + expected='^core .* [0-9]{2}-[0-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..750a2a02 --- /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 "not\[ \n\r\]*correct" { + exit 0 + } default { + exit 1 + } +} diff --git a/tests/main/manpages/task.yaml b/tests/main/manpages/task.yaml new file mode 100644 index 00000000..52c15827 --- /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 ! 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/op-install-failed-undone/task.yaml b/tests/main/op-install-failed-undone/task.yaml new file mode 100644 index 00000000..bbd65d31 --- /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 $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/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..84d5c7a6 --- /dev/null +++ b/tests/main/op-remove-retry/task.yaml @@ -0,0 +1,47 @@ +summary: Check that a remove operation is working even if the mount point is busy. + +restore: | + kill %1 || true + +execute: | + wait_for_service(){ + local service_name=$1 + local state=$2 + while ! systemctl show -p ActiveState $service_name | grep -q "ActiveState=$state"; do systemctl status $service_name || true; sleep 1; done + } + 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..24920f75 --- /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 $SNAPMOUNTDIR/"$snap_name"/ -maxdepth 1 -type d -name "x*" | wc -l) + } + + echo "Given two revisions of a snap have been installed" + snapbuild $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..5b5b15a0 --- /dev/null +++ b/tests/main/postrm-purge/task.yaml @@ -0,0 +1,28 @@ +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 + + echo "And snapd is purged" + # 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 + + . $TESTSLIB/dirs.sh + + echo "Nothing is left" + for d in $SNAPMOUNTDIR /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..1fb7c708 --- /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 $SNAPMOUNTDIR/bin/test_snapd_wellknown1 + test -h $SNAPMOUNTDIR/bin/test_snapd_wellknown2 + + echo "Disable the auto-aliases" + snap unalias test-snapd-auto-aliases + + echo "Auto-aliases are gone" + test ! -e $SNAPMOUNTDIR/bin/test_snapd_wellknown1 + test ! -e $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/bin/test_snapd_wellknown1 + test -h $SNAPMOUNTDIR/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..1a9fba99 --- /dev/null +++ b/tests/main/prepare-image-grub/task.yaml @@ -0,0 +1,78 @@ +summary: Check that prepare-image works for grub-systems +systems: [-ubuntu-core-16-*] +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/api/v1/snaps/ + + 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 <> $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..824ab601 --- /dev/null +++ b/tests/main/refresh-all/task.yaml @@ -0,0 +1,54 @@ +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 + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + echo "When the store is configured to make them refreshable" + . $TESTSLIB/store.sh + init_fake_refreshes test-snapd-tools,test-snapd-python-webserver $BLOB_DIR + + 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-core-with-failing-configure-hook/task.yaml b/tests/main/refresh-core-with-failing-configure-hook/task.yaml new file mode 100644 index 00000000..44ef4cd6 --- /dev/null +++ b/tests/main/refresh-core-with-failing-configure-hook/task.yaml @@ -0,0 +1,37 @@ +summary: Check refresh with a broken configure hook on core still works + +# FIXME: for now only +systems: [-ubuntu-core-*] + +environment: + BROKEN_CORE_SNAP: core_broken.snap + +restore: + rm -rf squashfs-root + rm -f $BROKEN_CORE_SNAP + +execute: | + snap list | awk "/^core / {print(\$3)}" > prevBoot + + echo "Breaking the configure hook of the core snap" + unsquashfs /var/lib/snapd/snaps/core_$(cat prevBoot).snap + printf '#!/bin/sh\necho somethingbroken\nfalse\n' > squashfs-root/meta/hooks/configure + chmod 755 squashfs-root/meta/hooks/configure + + . $TESTSLIB/snaps.sh + mksnap_fast "squashfs-root" "$BROKEN_CORE_SNAP" + rm -rf squshfs-root + + echo "Installing a new core which will trigger running the configure hook" + snap install --dangerous $BROKEN_CORE_SNAP + + echo "Checking changes" + . $TESTSLIB/changes.sh + snap changes + chg_id=$(change_id "Install \"core\" snap from file \"$BROKEN_CORE_SNAP\"" Done) + + echo "Verify the snap change" + snap change $chg_id | MATCH ".*ERROR ignoring failure in hook \"configure\".*" + snap change $chg_id | MATCH ".*somethingbroken.*" + + journalctl -u snapd | MATCH "Reported hook failure from \"configure\" for snap \"core\" as.*" diff --git a/tests/main/refresh-core-with-hanging-configure-hook/task.yaml b/tests/main/refresh-core-with-hanging-configure-hook/task.yaml new file mode 100644 index 00000000..a4e08dc7 --- /dev/null +++ b/tests/main/refresh-core-with-hanging-configure-hook/task.yaml @@ -0,0 +1,41 @@ +summary: Check refresh with a broken configure hook on core still works + +# FIXME: for now only +systems: [-ubuntu-core-*] + +environment: + BROKEN_CORE_SNAP: core_broken.snap + +restore: + rm -rf squashfs-root + rm -f $BROKEN_CORE_SNAP + +execute: | + snap list | awk "/^core / {print(\$3)}" > prevBoot + + echo "Breaking the configure hook of the core snap to hang forever" + unsquashfs /var/lib/snapd/snaps/core_$(cat prevBoot).snap + printf '#!/bin/sh\necho ithangs\nsleep 60\n' > squashfs-root/meta/hooks/configure + chmod 755 squashfs-root/meta/hooks/configure + + . $TESTSLIB/snaps.sh + mksnap_fast "squashfs-root" "$BROKEN_CORE_SNAP" + rm -rf squshfs-root + + echo "Installing a new core which will trigger running the configure hook" + snap install --dangerous $BROKEN_CORE_SNAP + + echo "Checking changes" + . $TESTSLIB/changes.sh + snap changes + chg_id=$(change_id "Install \"core\" snap from file \"$BROKEN_CORE_SNAP\"" Done) + + echo "Verify the snap change" + snap change $chg_id | MATCH ".*ERROR ignoring failure in hook \"configure\".*" + snap change $chg_id | MATCH ".*ithangs.*" + + # max-runtime set in prepare.sh via SNAPD_CONFIGURE_HOOK_TIMEOUT=30s + # in the environment + snap change $chg_id | MATCH ".*exceeded maximum runtime of 30s.*" + + journalctl -u snapd | MATCH "Reported hook failure from \"configure\" for snap \"core\" as.*" 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..915aa6b6 --- /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 test-snapd-tools $BLOB_DIR + 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..dfd925f8 --- /dev/null +++ b/tests/main/refresh-undo/task.yaml @@ -0,0 +1,45 @@ +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) + +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" + snapbuild $TESTSLIB/snaps/$SNAP_NAME_GOOD . + snapbuild $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..c996ac36 --- /dev/null +++ b/tests/main/refresh/task.yaml @@ -0,0 +1,109 @@ +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 + if [[ "$SPREAD_SYSTEM" == ubuntu-core-* ]]; then + exit + fi + 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 $SNAP_NAME $BLOB_DIR + 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 && "$SPREAD_SYSTEM" == ubuntu-core-* ]]; then + exit + 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..48875f22 --- /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 $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/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..5c2ffe0c --- /dev/null +++ b/tests/main/revert-devmode/task.yaml @@ -0,0 +1,81 @@ +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 test-snapd-tools $BLOB_DIR + 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 $SNAPMOUNTDIR/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 confined" + snap list|MATCH 'test-snapd-tools .* -' + + echo "When the latest revision is installed again" + snap remove --revision=$LATEST test-snapd-tools + snap refresh --devmode --edge test-snapd-tools + + echo "And revert is made with --devmode flag" + snap revert --devmode test-snapd-tools + + echo "Then snap uses devmode" + snap list|MATCH 'test-snapd-tools .* devmode' diff --git a/tests/main/revert-sideload/task.yaml b/tests/main/revert-sideload/task.yaml new file mode 100644 index 00000000..abee41a0 --- /dev/null +++ b/tests/main/revert-sideload/task.yaml @@ -0,0 +1,20 @@ +summary: Checks for snap sideload reverts + +prepare: | + snapbuild $TESTSLIB/snaps/basic . + +restore: | + rm ./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..b7139704 --- /dev/null +++ b/tests/main/revert/task.yaml @@ -0,0 +1,94 @@ +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 test-snapd-tools $BLOB_DIR + 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 "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 $SNAPMOUNTDIR/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 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/task.yaml b/tests/main/security-device-cgroups/task.yaml new file mode 100644 index 00000000..fe5476b4 --- /dev/null +++ b/tests/main/security-device-cgroups/task.yaml @@ -0,0 +1,69 @@ +summary: Ensure that the security rules related to device cgroups work. + +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 + +restore: | + 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 + + # TODO: check device unassociated after removing the udev file and rebooting diff --git a/tests/main/security-devpts/pts.exp b/tests/main/security-devpts/pts.exp new file mode 100644 index 00000000..86f26184 --- /dev/null +++ b/tests/main/security-devpts/pts.exp @@ -0,0 +1,38 @@ +#!/usr/bin/expect -f + +set timeout 20 + +spawn bash +send "ls /dev/pts\n" +expect "\[0-9]*ptmx" {} timeout { exit 1 } + +# Launch app and checks contents of /dev/pts, should have only ptmx +spawn su -l -c "/snap/bin/test-snapd-tools.sh" test +expect "bash-4.3\\$" {} timeout { exit 1 } +send "ls /dev/pts\n" +expect { + timeout { exit 1 } + "\[0-9]" { exit 1 } + "ptmx" +} + +# From within confined app, open a pty +send "python3\n" +expect ">>>" {} timeout { exit 1 } +send "import os, pty, sys\n" +send "os.listdir('/dev/pts')\n" +expect "\['ptmx'\]" {} timeout { exit 1 } +send "pty.openpty()\n" +send "os.listdir('/dev/pts')\n" +expect "\['0', 'ptmx'\]" {} timeout { exit 1 } +send "sys.exit(0)\n" +expect "bash-4.3\\$" {} timeout { exit 1 } + +# From with confined app, verify pty was closed (assumes that the last program +# closes it properly, which the above does) +send "ls /dev/pts\n" +expect { + timeout { exit 1 } + "\[0-9]" { exit 1 } + "ptmx" +} diff --git a/tests/main/security-devpts/task.yaml b/tests/main/security-devpts/task.yaml new file mode 100644 index 00000000..7881cb5d --- /dev/null +++ b/tests/main/security-devpts/task.yaml @@ -0,0 +1,13 @@ +summary: Ensure that the basic devpts security rules are in place. + +# ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 +systems: [-ubuntu-core-16-*, -ubuntu-*-ppc64el] + +prepare: | + echo "Given a basic snap is installed" + . $TESTSLIB/snaps.sh + install_local test-snapd-tools + +execute: | + echo "Then the pts device follows confinement rules" + expect -d -f pts.exp diff --git a/tests/main/security-private-tmp/task.yaml b/tests/main/security-private-tmp/task.yaml new file mode 100644 index 00000000..afef48c7 --- /dev/null +++ b/tests/main/security-private-tmp/task.yaml @@ -0,0 +1,48 @@ +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 + snapbuild $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: | + echo "When a temporary file is created by one snap" + expect -d -f tmp-create.exp + + if [ -e /usr/lib/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..18ec89ec --- /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 "/snap/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..39172a4d --- /dev/null +++ b/tests/main/security-profiles/task.yaml @@ -0,0 +1,31 @@ +summary: Check security profile generation for apps and hooks. + +prepare: | + snapbuild $TESTSLIB/snaps/basic-hooks . +restore: | + rm 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..a41f34d2 --- /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 $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/core/current/usr/lib/snapd/snap-confine || true + ls -ld $SNAPMOUNTDIR/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..9294414d --- /dev/null +++ b/tests/main/server-snap/task.yaml @@ -0,0 +1,34 @@ +summary: Check snap web servers + +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 <. + */ + +#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-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..66792b4f --- /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" + snapbuild $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..9381a8c4 --- /dev/null +++ b/tests/main/snap-download/task.yaml @@ -0,0 +1,32 @@ +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 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..a3d7ca98 --- /dev/null +++ b/tests/main/snap-env/task.yaml @@ -0,0 +1,53 @@ +summary: inspect all the set environment variables prefixed with SNAP_ and XDG_ +prepare: | + snapbuild $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_' | egrep -v '^SNAP_DID_REEXEC'| 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/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 + + . "$TESTSLIB/dirs.sh" + + echo "Ensure that SNAP, PATH and HOME are what we expect" + MATCH "^SNAP=$SNAPMOUNTDIR/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..e0ff3fc1 --- /dev/null +++ b/tests/main/snap-get/task.yaml @@ -0,0 +1,52 @@ +summary: Check that `snap get` works as expected + +prepare: | + echo "Build basic test package (without hooks)" + snapbuild $TESTSLIB/snaps/basic . + snap install --dangerous basic_1.0_all.snap + + echo "Build package with hook to run snapctl set" + snapbuild $TESTSLIB/snaps/snapctl-hooks . + snap install --dangerous snapctl-hooks_1.0_all.snap + +restore: | + rm basic_1.0_all.snap + rm 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 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 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..3980d43a --- /dev/null +++ b/tests/main/snap-info/task.yaml @@ -0,0 +1,28 @@ +summary: Check that snap info works + +prepare: | + . $TESTSLIB/pkgdb.sh + distro_install_package python3-yaml + + snapbuild $TESTSLIB/snaps/basic . + snap install test-snapd-tools + snap install --channel beta --devmode test-snapd-devmode + +restore: | + rm 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:" 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..b20f5750 --- /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 $SNAPMOUNTDIR/core/current) +restore: | + . $TESTSLIB/dirs.sh + mount --make-rshared / + mount --make-rshared $(readlink -f $SNAPMOUNTDIR/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 $SNAPMOUNTDIR" + cat /proc/self/mountinfo |MATCH "$SNAPMOUNTDIR $SNAPMOUNTDIR.*shared:[0-9]" + + echo "Run it again for good measure" + test-snapd-tools.echo hello + echo "... and ensure we do not mount $SNAPMOUNTDIR again" + n=$(cat /proc/self/mountinfo |grep "$SNAPMOUNTDIR $SNAPMOUNTDIR.*shared:[0-9]"|wc -l) + if [ "$n" -ne 1 ]; then + echo "Incorrect extra $SNAPMOUNTDIR bind mounts created" + exit 1 + fi \ No newline at end of file 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..a99f6ce3 --- /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 $SNAPMOUNTDIR/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..9fb47d17 --- /dev/null +++ b/tests/main/snap-repair/task.yaml @@ -0,0 +1,20 @@ +summary: Ensure that snap-repair is available + +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 snap-repair.timer + + echo "Check that snap-repair can be run" + "${LIBEXECDIR}"/snapd/snap-repair run | MATCH "run is not implemented yet" + + diff --git a/tests/main/snap-run-alias/task.yaml b/tests/main/snap-run-alias/task.yaml new file mode 100644 index 00000000..9311d487 --- /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 $SNAPMOUNTDIR/bin/test_echo + rm -f $SNAPMOUNTDIR/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=$SNAPMOUNTDIR/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 $SNAPMOUNTDIR/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..9bd4f0dd --- /dev/null +++ b/tests/main/snap-run-hook/task.yaml @@ -0,0 +1,48 @@ +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 + +prepare: | + echo "Build test hooks package" + snapbuild $TESTSLIB/snaps/basic-hooks . + snap install --dangerous basic-hooks_1.0_all.snap + +restore: | + rm basic-hooks_1.0_all.snap + +restore: | + rm 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 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..faac62e1 --- /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 $SNAPMOUNTDIR/bin/xxx + rmdir $SNAPMOUNTDIR/bin +execute: | + . $TESTSLIB/dirs.sh + echo Setting up incorrect symlink for snap run + mkdir -p $SNAPMOUNTDIR/bin + ln -s /usr/bin/snap $SNAPMOUNTDIR/bin/xxx + echo Running unknown command + expected="internal error, please report: running \"xxx\" failed: cannot find current revision for snap xxx: readlink $SNAPMOUNTDIR/xxx/current: no such file or directory" + output="$($SNAPMOUNTDIR/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..fe1a8651 --- /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=$SNAPMOUNTDIR/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 $SNAPMOUNTDIR/bin/$APP + ln -s /usr/bin/snap $SNAPMOUNTDIR/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..14a6760c --- /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 $SNAPMOUNTDIR/test-snapd-tools/current) + if [ -z "$CURRENT" ]; then + echo "Could not determine current version of $SNAP" + exit 1 + fi + + $SNAPMOUNTDIR/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 + $SNAPMOUNTDIR/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 + $SNAPMOUNTDIR/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-seccomp/task.yaml b/tests/main/snap-seccomp/task.yaml new file mode 100644 index 00000000..89255a88 --- /dev/null +++ b/tests/main/snap-seccomp/task.yaml @@ -0,0 +1,127 @@ +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..ab27311e --- /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] + +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-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/snapctl-from-snap/task.yaml b/tests/main/snapctl-from-snap/task.yaml new file mode 100644 index 00000000..a4a76f96 --- /dev/null +++ b/tests/main/snapctl-from-snap/task.yaml @@ -0,0 +1,68 @@ +summary: Check that `snapctl` can be run from the snap + +prepare: | + snap install --devmode jq + echo "Build basic test package" + snapbuild $TESTSLIB/snaps/snapctl-from-snap . + +restore: | + rm snapctl-from-snap_1.0_all.snap + +execute: | + check_cookie_exists() { + COOKIE_FILE=$1 + if ! test -f $COOKIE_FILE ; then + echo "Cookie file $COOKIE_FILE is missing" + exit 1 + fi + if [ $(stat -c %a $COOKIE_FILE) != "600" ]; then + echo "Incorrect permissions of file $COOKIE_FILE" + exit 1 + fi + wc -c $COOKIE_FILE | MATCH 44 + } + + snap install --dangerous snapctl-from-snap_1.0_all.snap + + COOKIE_FILE=/var/lib/snapd/cookie/snap.snapctl-from-snap + echo "Verify that cookie file exists and has proper permissions and size" + check_cookie_exists $COOKIE_FILE + + echo "Simulate upgrade from old snapd with no cookie support" + systemctl stop snapd.{service,socket} + rm -rf $COOKIE_FILE + jq -c 'del(.data["snap-cookies"])' /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 "Verify that cookie file was re-created" + check_cookie_exists $COOKIE_FILE + + echo "Verify that snapctl get can be executed by the app and shows the value set by configure hook" + /snap/bin/snapctl-from-snap.snapctl-get foo | MATCH bar + + echo "Verify that snapctl set can modify configuration values" + /snap/bin/snapctl-from-snap.snapctl-set foo=123 + /snap/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 + + echo "And a single revision gets removed" + snap remove snapctl-from-snap --revision=x1 + + echo "Verify that cookie file is still present" + if ! test -f $COOKIE_FILE ; then + echo "Cookie file $COOKIE_FILE is missing" + exit 1 + fi + + 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/task.yaml b/tests/main/snapctl/task.yaml new file mode 100644 index 00000000..a33726a1 --- /dev/null +++ b/tests/main/snapctl/task.yaml @@ -0,0 +1,26 @@ +summary: Check that `snapctl` can be run from within hooks + +prepare: | + snapbuild $TESTSLIB/snaps/snapctl-hooks . + snap install --dangerous snapctl-hooks_1.0_all.snap + +restore: | + rm 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" + 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 -q 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 -q 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..9800e4ca --- /dev/null +++ b/tests/main/snapd-reexec/task.yaml @@ -0,0 +1,93 @@ +summary: Test that snapd reexecs itself into core + +systems: [-ubuntu-core-16-*] + +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 $SNAPMOUNTDIR/core/current/usr/lib/snapd/info + fi + if mount|grep "/snap/core/.*/usr/lib/snapd/snapd"; then + umount $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/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..1dea6430 --- /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 /var/lib/snapd/seed/snaps/pc_x1.snap + cp seed.yaml.bak /var/lib/snapd/seed/seed.yaml + rm /var/lib/snapd/seed/assertions/developer1.account + rm /var/lib/snapd/seed/assertions/developer1.account-key + rm /var/lib/snapd/seed/assertions/developer1-pc.model + rm /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..e1a23890 --- /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 /var/lib/snapd/seed/snaps/pc_x1.snap + cp seed.yaml.bak /var/lib/snapd/seed/seed.yaml + rm /var/lib/snapd/seed/assertions/developer1.account + rm /var/lib/snapd/seed/assertions/developer1.account-key + rm /var/lib/snapd/seed/assertions/developer1-pc.model + rm /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..5d709993 --- /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 + +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 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/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..a7f31004 --- /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: | + snapbuild $TESTSLIB/snaps/data-writer . + +restore: | + rm 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/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..a1b8db87 --- /dev/null +++ b/tests/nested/core-revert/task.yaml @@ -0,0 +1,77 @@ +summary: core revert test + +systems: [ubuntu-16.04-64] + +prepare: | + apt install -y --no-install-recommends kpartx + + . "$TESTSLIB/nested.sh" + create_nested_core_vm + +restore: | + apt autoremove -y --purge kpartx + + . "$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..2ae15420 --- /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..3eb97343 --- /dev/null +++ b/tests/nightly/docker/task.yaml @@ -0,0 +1,43 @@ +summary: Check that the docker snap works +systems: [ubuntu-16.04-*, ubuntu-core-16-*, ubuntu-14.04-64] + +prepare: | + if apt show "linux-image-extra-$(uname -r)"; then + apt install -y "linux-image-extra-$(uname -r)" + fi + 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 + +restore: | + apt remove -y "linux-image-extra-$(uname -r)-generic" || true + +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..99843de5 --- /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 x11-utils xvfb unity + +disabled_restore: | + systemctl stop unity-app + apt autoremove -y --purge x11-utils xvfb 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..dc9356e0 --- /dev/null +++ b/tests/regression/lp-1595444/task.yaml @@ -0,0 +1,28 @@ +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-* +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..4af6c530 --- /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-*] +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..345d9580 --- /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-*] +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..2b1bdd81 --- /dev/null +++ b/tests/regression/lp-1599891/task.yaml @@ -0,0 +1,10 @@ +summary: Regression check for https://bugs.launchpad.net/snap-confine/+bug/1599891 +systems: + # No confinement (AppArmor, Seccomp) available on these systems + - -debian-* +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..d545ef91 --- /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-*] +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..66924020 --- /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 "$SNAPMOUNTDIR/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 + # - $SNAPMOUNTDIR/test-snapd-tools/$revision/bin + # - $SNAPMOUNTDIR/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..c6e44e8a --- /dev/null +++ b/tests/regression/lp-1641885/task.yaml @@ -0,0 +1,28 @@ +summary: snaps installed with --jailmode are not in devmode +systems: + # No confinement (AppArmor, Seccomp) available on these systems + - -debian-* +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: | + snapbuild "$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..39148d0a --- /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 $SNAPMOUNTDIR/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 $SNAPMOUNTDIR/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..0a074b17 --- /dev/null +++ b/tests/regression/lp-1704860/task.yaml @@ -0,0 +1,22 @@ +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. + snap run --shell test-snapd-classic-confinement ./snap-env-query.sh | MATCH -v 'SNAP_DID_REEXEC=' diff --git a/tests/unit/c-unit-tests/task.yaml b/tests/unit/c-unit-tests/task.yaml new file mode 100644 index 00000000..e641bb92 --- /dev/null +++ b/tests/unit/c-unit-tests/task.yaml @@ -0,0 +1,38 @@ +summary: Run the test suite for C code +environment: + EXTRA_PKGS: autoconf automake autotools-dev indent libapparmor-dev libglib2.0-dev libseccomp-dev libudev-dev pkg-config python3-docutils udev +prepare: | + # Sanity check, the core snap is installed + snap info core | MATCH "installed:" + # Install build dependencies for the test + dpkg --get-selections > pkg-list + apt-get install --yes $EXTRA_PKGS + # 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: | + # Refresh autotools build system + cd "$SPREAD_PATH/cmd/" + autoreconf --install --force + # Do an out-of-tree build in the autogarbage directory + mkdir -p "$SPREAD_PATH/cmd/autogarbage" + cd "$SPREAD_PATH/cmd/autogarbage" + EXTRA_CONF= + if [ ! -d /sys/kernel/security/apparmor ]; then + EXTRA_CONF="--disable-apparmor --disable-seccomp" + fi + "$SPREAD_PATH/cmd/configure" \ + --prefix=/usr --libexecdir=/usr/lib/snapd --enable-nvidia-ubuntu $EXTRA_CONF + # 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..4384663e --- /dev/null +++ b/tests/unit/gccgo/task.yaml @@ -0,0 +1,19 @@ +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 + apt install -y gccgo-6 + ln -s /usr/bin/go-6 /usr/local/bin/go +restore: | + rm -f /usr/local/bin/go + apt-get autoremove -y gccgo-6 +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..1665a6ff --- /dev/null +++ b/tests/unit/go/task.yaml @@ -0,0 +1,31 @@ +summary: Run project static and unit tests + +systems: [ubuntu-16.04-64] + +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 && 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 \ + 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..728a93da --- /dev/null +++ b/tests/upgrade/basic/task.yaml @@ -0,0 +1,72 @@ +summary: Check that upgrade works +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" + + echo Install previous version... + dpkg -l snapd snap-confine || true + apt-get install -y snapd + + prevsnapdver=$(snap --version|grep "snapd ") + + if [[ "$SPREAD_SYSTEM" = debian-* ]] ; then + # For debian we install the latest core snap from beta instead + # from stable as stable and candidate are broken at the moment. + # FIXME: drop this again once 2.25 landed in stable. + snap install --beta core + fi + + echo Install sanity check snaps with it + snap install test-snapd-tools + snap install test-snapd-auto-aliases + + 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 + distro_install_local_package --allow-downgrades "$GOHOME"/snapd*.deb + + 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 + + # 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/util/benchmark.sh b/tests/util/benchmark.sh new file mode 100755 index 00000000..6ae06b83 --- /dev/null +++ b/tests/util/benchmark.sh @@ -0,0 +1,22 @@ +#!/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 + spread -v "$BACKEND" + if [ "$?" = "0" ]; 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/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..574d8f86 --- /dev/null +++ b/timeutil/schedule.go @@ -0,0 +1,251 @@ +// -*- 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) +} + +// 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 and an optional weekday in which +// events should run. +type Schedule struct { + Start TimeOfDay + End TimeOfDay + + Weekday string +} + +func (sched *Schedule) String() string { + if sched.Weekday == "" { + return fmt.Sprintf("%s-%s", sched.Start, sched.End) + } + return fmt.Sprintf("%s@%s-%s", sched.Weekday, sched.Start, sched.End) +} + +func (sched *Schedule) Next(last time.Time) (start, end time.Time) { + now := timeNow() + wd := time.Weekday(weekdayMap[sched.Weekday]) + + 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) + + // we have not hit the right day yet + if sched.Weekday != "" && a.Weekday() != wd { + continue + } + // 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, +} + +// parseWeekday gets an input like "mon@9:00-11:00" or "9:00-11:00" +// and extracts the weekday of that schedule string (which can be +// empty). It returns the remainder of the string, the weekday +// and an error. +func parseWeekday(s string) (weekday, rest string, err error) { + if !strings.Contains(s, "@") { + return "", s, nil + } + s = strings.ToLower(s) + l := strings.SplitN(s, "@", 2) + weekday = l[0] + _, ok := weekdayMap[weekday] + if !ok { + return "", "", fmt.Errorf(`cannot parse %q, want "mon", "tue", etc`, l[0]) + } + rest = l[1] + + return weekday, rest, nil +} + +// 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) { + if strings.Contains(s, "@") { + return start, end, fmt.Errorf("cannot parse %q: contains invalid @", s) + } + 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 "mon@9:00-11:00" or +// "9:00-11:00" and returns a Schedule struct and an error. +func parseSingleSchedule(s string) (*Schedule, error) { + weekday, rest, err := parseWeekday(s) + if err != nil { + return nil, err + } + start, end, err := parseTimeInterval(rest) + if err != nil { + return nil, err + } + + return &Schedule{ + Weekday: weekday, + 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) +// thu@9:00-15:00 (only Thursday between 9am and 3pm) +// fri@9:00-11:00/mon@13:00-15:00 (only Friday between 9am and 3pm and Monday between 1pm and 3pm) +// fri@9:00-11:00/13:00-15:00 (only Friday between 9am and 3pm and every day between 1pm and 3pm) +// +// 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..bdf52963 --- /dev/null +++ b/timeutil/schedule_test.go @@ -0,0 +1,262 @@ +// -*- 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) 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"}, + {timeutil.Schedule{Start: timeutil.TimeOfDay{Hour: 13, Minute: 41}, End: timeutil.TimeOfDay{Hour: 14, Minute: 59}, Weekday: "mon"}, "mon@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`}, + // FIXME: error message sucks + {"9:00-mon@11:00", nil, `cannot parse "9:00-mon", want "mon", "tue", etc`}, + + // valid + {"9:00-11:00", []*timeutil.Schedule{{Start: timeutil.TimeOfDay{Hour: 9}, End: timeutil.TimeOfDay{Hour: 11}}}, ""}, + {"mon@9:00-11:00", []*timeutil.Schedule{{Weekday: "mon", 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}}}, ""}, + {"mon@9:00-11:00/Wed@22:00-23:00", []*timeutil.Schedule{{Weekday: "mon", Start: timeutil.TimeOfDay{Hour: 9}, End: timeutil.TimeOfDay{Hour: 11}}, {Weekday: "wed", Start: timeutil.TimeOfDay{Hour: 22}, End: timeutil.TimeOfDay{Hour: 23}}}, ""}, + } { + schedule, err := timeutil.ParseSchedule(t.in) + if t.errStr != "" { + c.Check(err, ErrorMatches, t.errStr, Commentf("%q returned unexpected error: %s", 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 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", + }, + { + // weekly schedule, next window today + schedule: "tue@9:00-11:00/wed@9:00-11:00", + last: "2017-02-01 10:00", + now: "2017-02-07 05:00", + next: "4h-6h", + }, + { + // weekly schedule, next window tomorrow + // (2017-02-06 is a monday) + schedule: "tue@9:00-11:00/wed@9:00-11:00", + last: "2017-02-06 03:00", + now: "2017-02-06 05:00", + next: "28h-30h", + }, + { + // weekly schedule, next window in 2 days + // (2017-02-06 is a monday) + schedule: "wed@9:00-11:00/thu@9:00-11:00", + last: "2017-02-06 03:00", + now: "2017-02-06 05:00", + next: "52h-54h", + }, + { + // weekly schedule, missed weekly window + // run next monday + schedule: "mon@9:00-11:00", + last: "2017-01-30 10:00", + now: "2017-02-06 12:00", + // 7*24h - 3h + next: "165h-167h", + }, + { + // multi day schedule, next window soon + schedule: "mon@9:00-11:00/tue@21:00-23:00", + last: "2017-01-31 22:00", + now: "2017-02-06 5:00", + next: "4h-6h", + }, + { + // weekly schedule, missed weekly window + // by more than 14 days + schedule: "mon@9:00-11:00", + last: "2017-01-01 10:00", + now: "2017-02-06 12:00", + next: "0s-0s", + }, + { + // 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/vendor/vendor.json b/vendor/vendor.json new file mode 100644 index 00000000..94cd031f --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,138 @@ +{ + "comment": "", + "ignore": "test", + "package": [ + { + "path": "context", + "revision": "" + }, + { + "checksumSHA1": "f4UJKXymPS31lDRMfsmPA4PvwVw=", + "path": "github.com/cheggaaa/pb", + "revision": "6e9d17711bb763b26b68b3931d47f24c1323abab", + "revisionTime": "2016-08-12T10:57:48Z" + }, + { + "checksumSHA1": "ab0MZfSfbTzqPQ1lVEJWpzZ5PJM=", + "path": "github.com/coreos/go-systemd/activation", + "revision": "f743bc15d6bddd23662280b4ad20f7c874cdd5ad", + "revisionTime": "2014-05-03T19:37:39Z" + }, + { + "checksumSHA1": "iIUYZyoanCQQTUaWsu8b+iOSPt4=", + "path": "github.com/gorilla/context", + "revision": "1c83b3eabd45b6d76072b66b746c20815fb2872d", + "revisionTime": "2015-08-20T05:12:45Z" + }, + { + "checksumSHA1": "8jXj6IMkk1B0LdIYgVG5Vn01/NU=", + "path": "github.com/gorilla/mux", + "revision": "ee1815431e497d3850809578c93ab6705f1a19f7", + "revisionTime": "2015-08-20T05:15:06Z" + }, + { + "checksumSHA1": "Fsff4Yngdyqbq9ulSyTT4LGrxck=", + "path": "github.com/jessevdk/go-flags", + "revision": "6b9493b3cb60367edd942144879646604089e3f7", + "revisionTime": "2016-02-27T09:34:14Z" + }, + { + "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": "4ZkAtTGnZIVrA5hUBCN/5+XdHYU=", + "path": "github.com/mvo5/net/bpf", + "revision": "0c0cd2628b36f2b053b7cc1ec04b76aab4c5189c", + "revisionTime": "2017-05-27T09:53:06Z" + }, + { + "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": "E28iXtNf9DZVsymruqAnTFGNNnE=", + "path": "golang.org/x/crypto", + "revision": "351dc6a5bf92a5f2ae22fadeee08eb6a45aa2d93", + "revisionTime": "2016-08-24T13:50:57Z", + "tree": true + }, + { + "checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=", + "path": "golang.org/x/net/context", + "revision": "6250b412798208e6c90b03b7c4f226de5aa299e2", + "revisionTime": "2016-08-24T22:20:41Z" + }, + { + "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=", + "path": "golang.org/x/net/context/ctxhttp", + "revision": "6250b412798208e6c90b03b7c4f226de5aa299e2", + "revisionTime": "2016-08-24T22:20:41Z" + }, + { + "checksumSHA1": "XeOje6FklwIZfFMTgE583al/ZDo=", + "path": "gopkg.in/check.v1", + "revision": "64131543e7896d5bcc6bd5a76287eb75ea96c673", + "revisionTime": "2014-10-24T13:38:53Z" + }, + { + "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": "TXfll3dCoaPKSntwHO0yR8eZ4JY=", + "path": "gopkg.in/yaml.v2", + "revision": "49c95bdc21843256fb6c4e0d370a05f24a0bf213", + "revisionTime": "2015-02-24T22:57:58Z" + } + ], + "rootPath": "github.com/snapcore/snapd" +} diff --git a/wrappers/binaries.go b/wrappers/binaries.go new file mode 100644 index 00000000..fceb3c69 --- /dev/null +++ b/wrappers/binaries.go @@ -0,0 +1,59 @@ +// -*- 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/snap" +) + +// AddSnapBinaries writes the wrapper binaries for the applications from the snap which aren't services. +func AddSnapBinaries(s *snap.Info) error { + if err := os.MkdirAll(dirs.SnapBinariesDir, 0755); err != nil { + return err + } + + for _, app := range s.Apps { + if app.IsService() { + continue + } + + if err := os.Remove(app.WrapperPath()); err != nil && !os.IsNotExist(err) { + return err + } + if err := os.Symlink("/usr/bin/snap", app.WrapperPath()); err != nil { + 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()) + } + + return nil +} diff --git a/wrappers/binaries_test.go b/wrappers/binaries_test.go new file mode 100644 index 00000000..a1bdcea1 --- /dev/null +++ b/wrappers/binaries_test.go @@ -0,0 +1,83 @@ +// -*- 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 ( + "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 + svc1: + command: bin/hello + stop-command: bin/goodbye + post-stop-command: bin/missya + daemon: forking +` +const contentsHello = "HELLO" + +func (s *binariesTestSuite) TestAddSnapBinariesAndRemove(c *C) { + info := snaptest.MockSnap(c, packageHello, contentsHello, &snap.SideInfo{Revision: snap.R(11)}) + + err := wrappers.AddSnapBinaries(info) + c.Assert(err, IsNil) + + link := filepath.Join(dirs.SnapBinariesDir, "hello-snap.hello") + target, err := os.Readlink(link) + c.Assert(err, IsNil) + c.Check(target, Equals, "/usr/bin/snap") + + err = wrappers.RemoveSnapBinaries(info) + c.Assert(err, IsNil) + + c.Check(osutil.FileExists(link), Equals, false) +} diff --git a/wrappers/desktop.go b/wrappers/desktop.go new file mode 100644 index 00000000..75843ff9 --- /dev/null +++ b/wrappers/desktop.go @@ -0,0 +1,224 @@ +// -*- 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) { + cmd := strings.SplitN(line, "=", 2)[1] + for _, app := range s.Apps { + env := fmt.Sprintf("env BAMF_DESKTOP_FILE_HINT=%s ", desktopFile) + 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 + } + } + + 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) error { + 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 + } + } + + // 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..a87be3c3 --- /dev/null +++ b/wrappers/desktop_test.go @@ -0,0 +1,315 @@ +// -*- 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}, + }) +} + +// 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) 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..d2b977e0 --- /dev/null +++ b/wrappers/services.go @@ -0,0 +1,272 @@ +// -*- 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" + "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) + 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") + } + + 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 err := sysd.Start(app.ServiceName()); 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) error { + sysd := systemd.New(dirs.GlobalRootDir, inter) + nservices := 0 + + for _, app := range s.Apps { + if !app.IsService() { + continue + } + nservices++ + // 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 + } + if err := sysd.Enable(app.ServiceName()); err != nil { + return err + } + } + + if nservices > 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()) + 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) + } + + if err := os.Remove(app.ServiceSocketFile()); err != nil && !os.IsNotExist(err) { + logger.Noticef("Failed to remove socket 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}} + +[Install] +WantedBy={{.ServicesTarget}} +` + var templateOut bytes.Buffer + t := template.Must(template.New("service-wrapper").Parse(serviceTemplate)) + + var restartCond string + if appInfo.RestartCond == systemd.RestartNever { + restartCond = "no" + } else { + restartCond = appInfo.RestartCond.String() + } + if restartCond == "" { + restartCond = systemd.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() +} diff --git a/wrappers/services_gen_test.go b/wrappers/services_gen_test.go new file mode 100644 index 00000000..27722354 --- /dev/null +++ b/wrappers/services_gen_test.go @@ -0,0 +1,238 @@ +// -*- 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/systemd" + "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[Install]\nWantedBy=multi-user.target") +) + +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 systemd.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 == systemd.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) +} diff --git a/wrappers/services_test.go b/wrappers/services_test.go new file mode 100644 index 00000000..c70ae56a --- /dev/null +++ b/wrappers/services_test.go @@ -0,0 +1,202 @@ +// -*- 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" + "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 + prevctlCmd func(...string) ([]byte, error) +} + +var _ = Suite(&servicesTestSuite{}) + +func (s *servicesTestSuite) SetUpTest(c *C) { + s.tempdir = c.MkDir() + dirs.SetRootDir(s.tempdir) + + s.prevctlCmd = systemd.SystemctlCmd + systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) { + return []byte("ActiveState=inactive\n"), nil + } +} + +func (s *servicesTestSuite) TearDownTest(c *C) { + dirs.SetRootDir("") + systemd.SystemctlCmd = s.prevctlCmd +} + +func (s *servicesTestSuite) TestAddSnapServicesAndRemove(c *C) { + var sysdLog [][]string + systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + return []byte("ActiveState=inactive\n"), nil + } + + 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.NullProgress{}) + 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.NullProgress{}) + 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) TestRemoveSnapPackageFallbackToKill(c *C) { + restore := wrappers.MockKillWait(200 * time.Millisecond) + defer restore() + + var sysdLog [][]string + systemd.SystemctlCmd = 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 + } + + 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.NullProgress{}) + 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) TestStartServices(c *C) { + var sysdLog [][]string + systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) { + sysdLog = append(sysdLog, cmd) + return []byte("ActiveState=inactive\n"), nil + } + + 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) TestStartSnapMultiServicesFailStartCleanup(c *C) { + var sysdLog [][]string + svc1Name := "snap.hello-snap.svc1.service" + svc2Name := "snap.hello-snap.svc2.service" + numStarts := 0 + + systemd.SystemctlCmd = 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 { + // order flipped + svc1Name, svc2Name = svc2Name, svc1Name + } + return nil, fmt.Errorf("failed") + } + } + return []byte("ActiveState=inactive\n"), nil + } + + 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