#!/usr/bin/python2
# -*- coding: utf-8 -*-

# This file is part of Cockpit.
#
# Copyright (C) 2017 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import os

import parent
from testlib import *
from packagelib import PackageCase

WAIT_SCRIPT = """
set -ex
for x in $(seq 1 200); do
    if curl --insecure -s https://%(addr)s:8443/candlepin; then
        break
    else
        sleep 1
    fi
done
"""

@skipImage("Image uses OSTree", "continuous-atomic", "fedora-atomic", "rhel-atomic")
class TestUpdates(PackageCase):
    def setUp(self):
        PackageCase.setUp(self)

        # Disable Subscription Manager for these tests; subscriptions are tested in a separate class
        self.machine.execute("[ ! -f /usr/libexec/rhsmd ] || mv /usr/libexec/rhsmd /usr/libexec/rhsmd.disabled")

    def testBasic(self):
        # no security updates, no changelogs
        b = self.browser
        m = self.machine

        m.start_cockpit()
        b.login_and_go("/updates")

        # no repositories at all, thus no updates
        b.wait_present(".content-header-extra td button")
        b.wait_in_text("#state", "No updates pending")
        b.wait_present(".content-header-extra td.text-right span")
        # PK starts from a blank state, thus should force refresh and set the "time since" to 0
        self.assertEqual(b.text(".content-header-extra td.text-right span"), "Last checked: a few seconds ago")
        # empty state visible in main area
        b.wait_present(".container-fluid div.blank-slate-pf")

        install_lockfile = "/tmp/finish-pk"
        # create two updates; force installing chocolate before vanilla
        self.createPackage("vanilla", "1.0", "1", install=True)
        self.createPackage("vanilla", "1.0", "2", depends="chocolate",
                           postinst="while [ ! -e {0} ]; do sleep 1; done; rm -f {0}".format(install_lockfile))
        self.createPackage("chocolate", "2.0", "1", install=True)
        self.createPackage("chocolate", "2.0", "2")
        self.enableRepo()

        # check again
        b.wait_in_text(".content-header-extra td button", "Check for Updates")
        b.click(".content-header-extra td button")

        b.wait_present(".content-header-extra td.text-right span")
        self.assertEqual(b.text(".content-header-extra td.text-right span"), "Last checked: a few seconds ago")

        b.wait_present("#available h2")
        b.wait_in_text("#available h2", "Available Updates")
        self.assertEqual(b.text("#state"), "2 updates")

        b.wait_present("table.listing-ct")
        b.wait_in_text("table.listing-ct", "vanilla")

        # chocolate update to 2.0-2
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(1) th span"), "chocolate")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(1) th .tooltip-inner"), "dummy chocolate")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(1) td:nth-of-type(1)"), "2.0-2")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(1) td:nth-of-type(2)"), "")  # no bugs

        # vanilla update to 1.0-2
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(2) th span"), "vanilla")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(2) th .tooltip-inner"), "dummy vanilla")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(2) td:nth-of-type(1)"), "1.0-2")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(2) td:nth-of-type(2)"), "")  # no bugs

        # old versions are still installed
        m.execute("test -f /stamp-vanilla-1.0-1 && test -f /stamp-chocolate-2.0-1")

        # no update history yet
        self.assertFalse(b.is_present("#history"))

        # should only have one button (no security updates)
        self.assertEqual(b.text("#app .container-fluid button"), "Install All Updates")
        b.click("#app .container-fluid button")

        b.wait_in_text("#state", "Applying updates")
        b.wait_present("#app div.progress-bar")

        # no refresh button or "last checked", but Cancel button
        b.wait_present(".content-header-extra td.text-right button")
        self.assertFalse(b.is_present(".content-header-extra td.text-right span"))
        b.wait_in_text(".content-header-extra td button", "Cancel")

        # Cancel button should eventually get disabled
        b.wait_present(".content-header-extra td button:disabled")

        # update log only exists in the expander, collapsed by default
        self.assertFalse(b.is_present("#update-log"))
        # expand it
        b.wait_present(".expander-title span")
        b.click(".expander-title span")
        b.wait_present("#update-log")
        b.wait_visible("#update-log")
        # should eventually show chocolate when vanilla starts installing
        b.wait_in_text("#update-log", "chocolate")

        # finish the package installation
        m.execute("touch {0}".format(install_lockfile))

        # update should succeed and show restart page; cancel
        b.wait_present("#app .container-fluid h1")
        b.wait_in_text("#app .container-fluid h1", "Restart Recommended")
        b.wait_present("#app .container-fluid button.btn-primary")
        self.assertEqual(b.text("#app .container-fluid button.btn-primary"), "Restart Now")
        b.wait_present("#app .container-fluid button.btn-default")
        self.assertEqual(b.text("#app .container-fluid button.btn-default"), "Ignore")
        b.click("#app .container-fluid button.btn-default")

        # should go back to updates overview, nothing pending any more
        b.wait_present("#state")
        b.wait_in_text("#state", "No updates pending")
        b.wait_present(".content-header-extra td.text-right span")
        b.wait_in_text(".content-header-extra td.text-right span", "Last checked:")

        # empty state visible in main area
        b.wait_present(".container-fluid div.blank-slate-pf")

        # new versions are now installed
        m.execute("test -f /stamp-vanilla-1.0-2 && test -f /stamp-chocolate-2.0-2")

    @skipImage("Ubuntu 16.04 PackageKit does not support changelogs", "ubuntu-1604")
    def testInfoSecurity(self):
        b = self.browser
        m = self.machine

        # just changelog
        self.createPackage("norefs-bin", "1", "1", install=True)
        self.createPackage("norefs-bin", "2", "1", severity="enhancement", changes="Now 10% more unicorns")
        # binary from same source
        self.createPackage("norefs-doc", "1", "1", install=True)
        self.createPackage("norefs-doc", "2", "1", severity="enhancement", changes="Now 10% more unicorns")
        # bug fixes
        self.createPackage("buggy", "2", "1", install=True)
        self.createPackage("buggy", "2", "2", changes="Fixit", bugs=[123, 456])
        # security fix with proper CVE list and severity
        self.createPackage("secdeclare", "3", "4.a1", install=True)
        self.createPackage("secdeclare", "3", "4.b1", severity="security",
                           changes="Will crash your data center", cves=['CVE-2014-123456'])
        # security fix with parsing from changes
        self.createPackage("secparse", "4", "1", install=True)
        self.createPackage("secparse", "4", "2", changes="Fix CVE-2014-54321 and CVE-2017-9999.")

        self.enableRepo()
        m.execute("pkcon refresh")

        m.start_cockpit()
        b.login_and_go("/updates")
        b.wait_present("#available h2")
        b.wait_in_text("#available h2", "Available Updates")
        self.assertEqual(b.text("#state"), "5 updates, including 2 security fixes")

        b.wait_present("table.listing-ct")
        b.wait_in_text("table.listing-ct", "secparse")

        # security updates should get sorted on top and then alphabetically, so start with "secdeclare"
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(1) th span"), "secdeclare")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(1) td:nth-of-type(1)"), "3-4.b1")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(1) td:nth-of-type(2)"), "")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(1) td:nth-of-type(3) span.security-label-text"),
                         "Security Update: ")
        desc = b.text("#app .listing-ct tbody:nth-of-type(1) td:nth-of-type(3)")
        self.assertIn("Will crash your data center", desc)
        self.assertIn("CVE-2014-123456", desc)

        # secparse should also be considered a security update as the changelog mentions CVEs
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(2) th span"), "secparse")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(2) td:nth-of-type(1)"), "4-2")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(2) td:nth-of-type(2)"), "")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(2) td:nth-of-type(3) span.security-label-text"),
                         "Security Update: ")
        desc = b.text("#app .listing-ct tbody:nth-of-type(2) td:nth-of-type(3)")
        self.assertIn("Fix CVE-2014-54321 and CVE-2017-9999.", desc)

        # buggy: bug refs, no security
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(3) th span"), "buggy")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(1)"), "2-2")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(2)"), "123, 456")
        self.assertIn("Fixit", b.text("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(3)"))

        # norefs: just changelog, show both binary packages
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(4) th > div:nth-of-type(1) span"), "norefs-bin")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(4) th > div:nth-of-type(2) span"), "norefs-doc")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(4) td:nth-of-type(1)"), "2-1")
        self.assertEqual(b.text("#app .listing-ct tbody:nth-of-type(4) td:nth-of-type(2)"), "")  # no bugs
        self.assertIn("Now 10% more unicorns", b.text("#app .listing-ct tbody:nth-of-type(4) td:nth-of-type(3)"))

        # install only security updates
        self.assertEqual(b.text("#app .container-fluid button.btn-default"), "Install Security Updates")
        b.click("#app .container-fluid button.btn-default")

        b.wait_in_text("#state", "Applying updates")
        b.wait_present("#app div.progress-bar")

        # should have succeeded and show restart page
        b.wait_present("#app .container-fluid h1")
        b.wait_in_text("#app .container-fluid h1", "Restart Recommended")

        def assertHistory(path, updates):
            selector = path + " .tooltip-ct-outer:nth-child({0}) li"
            b.wait_present(path)
            b.wait_present(selector.format(len(updates)))
            for index, pkg in enumerate(updates, start=1):
                self.assertEqual(b.text(selector.format(index)), pkg)
            # make sure we don't have any extra ones
            self.assertFalse(b.is_present(selector.format(len(updates) + 1)))

        # history on restart page should show the two security updates
        b.wait_present(".expander-title span")
        b.click(".expander-title span")
        assertHistory("ul", ["secdeclare", "secparse"])

        # ignore restarting
        b.wait_present("#app .container-fluid button.btn-default")
        b.click("#app .container-fluid button.btn-default")

        # should have succeeded; 3 non-security updates left
        b.wait_present("#state")
        b.wait_in_text("#state", "3 updates")
        b.wait_present(".container-fluid #available h2")
        b.wait_in_text(".container-fluid #available h2", "Available Updates")
        b.wait_in_text("table.listing-ct", "norefs-doc")
        self.assertIn("buggy", b.text("table.listing-ct"))
        self.assertNotIn("secdeclare", b.text("table.listing-ct"))
        self.assertNotIn("secparse", b.text("table.listing-ct"))

        # history should show the security updates
        b.wait_present(".container-fluid #history h2")
        assertHistory("#history ul", ["secdeclare", "secparse"])

        # new security versions are now installed
        m.execute("test -f /stamp-secdeclare-3-4.b1 && test -f /stamp-secparse-4-2")
        # but the three others are untouched
        m.execute("test -f /stamp-buggy-2-1 && test -f /stamp-norefs-bin-1-1 && test -f /stamp-norefs-doc-1-1")

        # should now only have one button (no security updates left)
        self.assertEqual(b.text("#app .container-fluid button"), "Install All Updates")
        b.click("#app .container-fluid button")

        b.wait_in_text("#state", "Applying updates")
        b.wait_present("#app div.progress-bar")

        # should have succeeded and show restart
        b.wait_present("#app .container-fluid h1")
        b.wait_in_text("#app .container-fluid h1", "Restart Recommended")
        b.wait_present("#app .container-fluid button.btn-default")

        # new versions are now installed
        m.execute("test -f /stamp-norefs-bin-2-1 && test -f /stamp-norefs-doc-2-1")

        # history on restart page should show the three non-security updates
        b.wait_present(".expander-title span")
        b.click(".expander-title span")
        assertHistory("ul", ["buggy", "norefs-bin", "norefs-doc"])

        # do the reboot; this will disconnect the web UI
        b.click("#app .container-fluid button.btn-primary")
        b.switch_to_top()
        b.wait_present(".curtains-ct")
        b.wait_visible(".curtains-ct")
        b.wait_in_text(".curtains-ct h1", "Disconnected")

        # ensure that rebooting actually worked
        m.wait_reboot()
        m.start_cockpit()
        b.reload()
        b.login_and_go("/updates")

        # no further updates
        b.wait_present("#state")
        b.wait_in_text("#state", "No updates pending")
        # empty state visible in main area
        b.wait_present(".container-fluid div.blank-slate-pf")

        # history on "up to date" page should show the recent update
        assertHistory("ul", ["buggy", "norefs-bin", "norefs-doc"])

        self.allow_restart_journal_messages()

    @skipImage("Ubuntu 16.04 PackageKit does not support changelogs", "ubuntu-1604")
    def testSecurityOnly(self):
        b = self.browser
        m = self.machine

        # security fix with proper CVE list and severity
        self.createPackage("secdeclare", "3", "4.a1", install=True)
        self.createPackage("secdeclare", "3", "4.b1", severity="security",
                           changes="Will crash your data center", cves=['CVE-2014-123456'])

        self.enableRepo()
        m.execute("pkcon refresh")

        m.start_cockpit()
        b.login_and_go("/updates")
        b.wait_present("#available h2")
        b.wait_in_text("#available h2", "Available Updates")
        self.assertEqual(b.text("#state"), "1 security fix")

        # should only have one button (only security updates)
        self.assertEqual(b.text("#app .container-fluid button"), "Install Security Updates")

    @skipImage("Ubuntu 16.04 PackageKit does not support changelogs", "ubuntu-1604")
    def testInfoTruncation(self):
        b = self.browser
        m = self.machine

        # update with not too many binary packages
        for i in range(8):
            self.createPackage("coarse{:02}".format(i), "1", "1", install=True)
            self.createPackage("coarse{:02}".format(i), "1", "2", changes="make it greener")

        # update with lots of binary packages
        for i in range(25):
            self.createPackage("fine{:02}".format(i), "1", "1", install=True)
            self.createPackage("fine{:02}".format(i), "1", "2", changes="make it better")

        # update with long changelog
        long_changelog = ""
        for i in range(30):
            long_changelog += " - Things change #{:02}\n".format(i)
        self.createPackage("verbose", "1", "1", install=True)
        self.createPackage("verbose", "1", "2", changes=long_changelog)

        # update with lots of packages and long changelog
        for i in range(20):
            self.createPackage("wide-n-tall{:02}".format(i), "3", "1", install=True)
            self.createPackage("wide-n-tall{:02}".format(i), "3", "2", changes=long_changelog)

        self.enableRepo()

        m.start_cockpit()
        b.login_and_go("/updates")

        b.wait_present("table.listing-ct")
        b.wait_in_text("table.listing-ct", "Things change #05")

        # "coarse" package list should be complete
        t = b.text("#app .listing-ct tbody:nth-of-type(1) th")
        self.assertIn("coarse00", t)
        self.assertIn("coarse07", t)
        self.assertNotIn("more", t)

        # "fine" package list should be truncated
        t = b.text("#app .listing-ct tbody:nth-of-type(2) th")
        self.assertIn("fine00", t)
        self.assertIn("fine10", t)
        self.assertNotIn("fine20", t)
        b.wait_present("#app .listing-ct tbody:nth-of-type(2) th a")
        b.wait_in_text("#app .listing-ct tbody:nth-of-type(2) th a", "10 more")

        # verbose changelog should be truncated
        t = b.text("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(3)")
        self.assertIn("Things change #00", t)
        self.assertIn("Things change #04", t)
        self.assertNotIn("Things change #07", t)
        self.assertNotIn("Things change #20", t)
        b.wait_present("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(3) a")
        b.wait_in_text("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(3) a", "More information")

        # wide-n-tall package list and changelog should both be truncated
        t = b.text("#app .listing-ct tbody:nth-of-type(4) td:nth-of-type(3)")
        self.assertIn("Things change #04", t)
        self.assertNotIn("Things change #07", t)
        b.wait_present("#app .listing-ct tbody:nth-of-type(4) th a")
        b.wait_in_text("#app .listing-ct tbody:nth-of-type(4) th a", "5 more")
        b.wait_present("#app .listing-ct tbody:nth-of-type(4) td:nth-of-type(3) a")
        b.wait_in_text("#app .listing-ct tbody:nth-of-type(4) td:nth-of-type(3) a", "More information")

        # expand fine package list by clicking on the "more" link
        b.click("#app .listing-ct tbody:nth-of-type(2) th a")
        b.wait_in_text("#app .listing-ct tbody:nth-of-type(2) th", "fine24")
        b.wait_not_present("#app .listing-ct tbody:nth-of-type(2) th a")

        # expand verbose changelog by clicking on he truncated text
        b.click("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(3) div")
        b.wait_in_text("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(3)", "Things change #29")
        b.wait_not_present("#app .listing-ct tbody:nth-of-type(3) td:nth-of-type(3) a")

        # clicking on package list should also expand changelog
        b.click("#app .listing-ct tbody:nth-of-type(4) th a")
        b.wait_in_text("#app .listing-ct tbody:nth-of-type(4) th", "wide-n-tall19")
        b.wait_in_text("#app .listing-ct tbody:nth-of-type(4) td:nth-of-type(3)", "Things change #29")
        b.wait_not_present("#app .listing-ct tbody:nth-of-type(4) td:nth-of-type(3) a")

    def testUpdateError(self):
        b = self.browser
        m = self.machine

        self.createPackage("vapor", "1", "1", install=True)
        self.createPackage("vapor", "1", "2")

        self.enableRepo()
        m.execute("pkcon refresh")

        # break the upgrade by removing the generated packages from the repo
        m.execute("rm -f /tmp/repo/vapor*.deb /tmp/repo/vapor*.rpm")

        m.start_cockpit()
        b.login_and_go("/updates")
        b.wait_present(".container-fluid h2")
        b.wait_in_text(".container-fluid h2", "Available Updates")
        self.assertEqual(b.text("#state"), "1 update")

        b.wait_present("#app .container-fluid button")
        b.click("#app .container-fluid button")

        b.wait_in_text("#state", "Applying updates failed")

        # expecting one error message, so this should be unique
        b.wait_present("#app .container-fluid pre")
        self.assertRegexpMatches(b.text("#app .container-fluid pre"), "missing|downloading|not.*available|No such file or directory")

        # not expecting any buttons
        self.assertFalse(b.is_present("#app button"))

    def testRunningUpdate(self):
        # The main case for this is that cockpit-ws itself gets upgraded, which
        # restarts the service and terminates the connection. As we can't
        # (efficiently) build a newer working cockpit-ws package, test the two
        # parts (reconnect and warning about disconnect) separately.

        # no security updates, no changelogs
        b = self.browser
        m = self.machine

        install_lockfile = "/tmp/finish-pk"
        # updating this package takes longer than a cockpit start and building the page
        self.createPackage("slow", "1", "1", install=True)
        self.createPackage("slow", "1", "2",  postinst="while [ ! -e {0} ]; do sleep 1; done; rm -f {0}".format(install_lockfile))
        self.enableRepo()
        m.execute("pkcon refresh")

        m.start_cockpit()
        b.login_and_go("/updates")

        b.wait_present("#app .container-fluid button")
        b.click("#app .container-fluid button")
        b.wait_in_text("#state", "Applying updates")
        b.wait_present("#app div.progress-description")
        b.wait_in_text("#app div.progress-description", "slow")

        # restarting should pick up that install progress
        m.restart_cockpit()
        b.login_and_go("/updates")
        b.wait_present("#state")
        b.wait_in_text("#state", "Applying updates")
        b.wait_present("#app div.progress-bar")

        # finish the package installation
        m.execute("touch {0}".format(install_lockfile))

        # should have succeeded and show restart page; cancel
        b.wait_present("#app .container-fluid h1")
        b.wait_in_text("#app .container-fluid h1", "Restart Recommended")
        b.wait_present("#app .container-fluid button.btn-default")
        b.click("#app .container-fluid button.btn-default")
        b.wait_present("#state")
        b.wait_in_text("#state", "No updates pending")

        # now pretend that there is a newer cockpit-ws available, warn about disconnect
        self.createPackage("cockpit-ws", "999", "1")
        self.createPackage("cockpit", "999", "1")  # as that depends on same version of ws
        self.enableRepo()
        b.wait_in_text(".content-header-extra td button", "Check for Updates")
        b.click(".content-header-extra td button")

        b.wait_present(".container-fluid #available h2")
        b.wait_in_text(".container-fluid #available h2", "Available Updates")
        self.assertEqual(b.text("#state"), "2 updates")

        b.wait_present("table.listing-ct")
        b.wait_in_text("table.listing-ct", "cockpit-ws")

        b.wait_present("#app div.alert-warning")
        b.wait_in_text("#app div.alert-warning", "Cockpit itself will be updated")

    def testPackageKitCrash(self):
        b = self.browser
        m = self.machine

        # make sure we have enough time to crash PK
        self.createPackage("slow", "1", "1", install=True)
        # we don't want this installation to finish
        self.createPackage("slow", "1", "2", postinst="sleep infinity")
        self.enableRepo()
        m.execute("pkcon refresh")

        m.start_cockpit()
        b.login_and_go("/updates")

        b.wait_present("#app .container-fluid button")
        b.click("#app .container-fluid button")

        # let updates start and zap PackageKit
        b.wait_present("#app div.progress-bar")
        m.execute("systemctl kill --signal=SEGV packagekit.service")

        b.wait_in_text("#state", "Applying updates failed")
        b.wait_present("#app .container-fluid pre")
        self.assertEqual(b.text("#app .container-fluid pre"), "PackageKit crashed")

        self.allow_journal_messages(".*org.freedesktop.PackageKit.*Error.NoReply.*")

    def testNoPackageKit(self):
        b = self.browser
        m = self.machine

        m.execute('''systemctl stop packagekit.service
                     rm `systemctl show -p FragmentPath packagekit.service | cut -f2 -d=`
                     rm /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service
                     systemctl daemon-reload''')

        m.start_cockpit()
        b.login_and_go("/updates")

        b.wait_present("#state")
        b.wait_in_text("#state", "Loading available updates failed")
        b.wait_present("#app pre")
        b.wait_in_text("#app pre", "PackageKit is not installed")


@skipImage("Image uses OSTree", "continuous-atomic", "fedora-atomic", "rhel-atomic")
@skipImage("No subscriptions", "centos-7", "debian-stable", "debian-testing", "fedora-26", "fedora-27", "fedora-i386", "fedora-testing", "ubuntu-1604", "ubuntu-stable")
class TestUpdatesSubscriptions(PackageCase):
    provision = {
        "0": { "address": "10.111.113.2/20", "dns": "10.111.113.2" },
        "candlepin": { "image": "candlepin", "address": "10.111.113.5/20" }
    }

    def register(self):
        # this fails with "Unable to find available subscriptions for all your installed products", but works anyway
        self.machine.execute("subscription-manager register --insecure --serverurl https://10.111.113.5:8443/candlepin --org=admin --activationkey=awesome_os_pool || true")
        self.machine.execute("subscription-manager attach --auto")

    def setUp(self):
        PackageCase.setUp(self)
        self.candlepin = self.machines['candlepin']
        m = self.machine

        # wait for candlepin to be active and verify
        self.candlepin.execute("systemctl start tomcat")

        # remove all existing products (RHEL server), as we can't control them
        m.execute("rm -f /etc/pki/product-default/*.pem /etc/pki/product/*.pem")

        # download product info from the candlepin machine and install it
        product_file = os.path.join(self.tmpdir, "88888.pem")
        self.candlepin.download("/root/candlepin/generated_certs/88888.pem", product_file)

        # # upload product info to the test machine
        m.upload([product_file], "/etc/pki/product")

        # make sure that rhsm skips certificate checks for the server
        m.execute("sed -i -e 's/insecure = 0/insecure = 1/g' /etc/rhsm/rhsm.conf")

        # Wait for the web service to be accessible
        m.execute(script=WAIT_SCRIPT % { "addr": "10.111.113.5" })

    def testNoUpdates(self):
        m = self.machine
        b = self.browser

        # fresh machine, no updates available; by default our rhel-* images are not registered
        m.start_cockpit()
        b.login_and_go("/updates")

        # empty state visible in main area
        b.wait_present(".container-fluid div.blank-slate-pf button")
        b.wait_in_text(".container-fluid div.blank-slate-pf", "This system is not registered")
        # hides the header bar
        self.assertFalse(b.is_present(".content-header-extra"))

        # use the button to switch to Subscriptions
        b.click(".container-fluid div.blank-slate-pf button")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname === "/subscriptions"')

        # after registration it should show the usual "system is up to date", through the "status changed" signal
        self.register()
        b.go("/updates")
        b.enter_page("/updates")
        b.wait_present(".content-header-extra td button")
        b.wait_present("#state")
        b.wait_in_text("#state", "No updates pending")
        b.wait_present(".container-fluid div.blank-slate-pf")
        b.wait_in_text(".container-fluid div.blank-slate-pf", "up to date")

    def testAvailableUpdates(self):
        m = self.machine
        b = self.browser

        # one available update
        self.createPackage("vanilla", "1.0", "1", install=True)
        self.createPackage("vanilla", "1.0", "2")
        self.enableRepo()

        # by default our rhel-* images are not registered; should show available update and unregistered warning, but no
        # other action buttons
        m.start_cockpit()
        b.login_and_go("/updates")

        b.wait_present(".alert-warning")
        b.wait_in_text(".alert-warning", "subscribe")
        b.wait_present("table.listing-ct")
        b.wait_in_text("table.listing-ct", "vanilla")
        # should show header bar
        b.wait_present("#state")
        self.assertEqual(b.text("#state"), "1 update")
        b.wait_present(".content-header-extra")
        self.assertEqual(b.text(".content-header-extra td.text-right span"), "Last checked: a few seconds ago")
        # should be unique; no other action buttons
        b.wait_present("button")
        b.wait_in_text("button", "Registration")

        # use the button to switch to Subscriptions
        b.click("button")
        b.switch_to_top()
        b.wait_js_cond('window.location.pathname === "/subscriptions"')

        # after registration it should show the usual "system is up to date"
        self.register()
        b.go("/updates")
        b.enter_page("/updates")
        b.wait_not_present(".alert-warning")
        # no update history yet, no warning any more
        b.wait_present("#available h2")
        b.wait_in_text("#available h2", "Available Updates")
        self.assertFalse(b.is_present("#history"))

        # has action buttons
        b.wait_present(".content-header-extra td button")
        b.wait_present("#app .container-fluid button")
        self.assertEqual(b.text("#app .container-fluid button"), "Install All Updates")


@skipImage("Image uses OSTree", "continuous-atomic", "fedora-atomic", "rhel-atomic")
class TestAutoUpdates(PackageCase):
    def setUp(self):
        PackageCase.setUp(self)

        # Disable Subscription Manager for these tests; subscriptions are tested in a separate class
        self.machine.execute("[ ! -f /usr/libexec/rhsmd ] || mv /usr/libexec/rhsmd /usr/libexec/rhsmd.disabled")

        # expected backend; hardcode this on image names to check the auto-detection
        if self.machine.image.startswith("debian") or self.machine.image.startswith("ubuntu"):
            self.backend = "apt"
        elif self.machine.image.startswith("fedora"):
            self.backend = "dnf"
        elif self.machine.image in ["centos-7", "rhel-7", "rhel-7-4", "rhel-7-5"]:
            self.backend = "yum"
        else:
            raise NotImplementedError("unknown image " + self.machine.image)

        # not implemented for yum and apt yet, only dnf
        self.supported_backend = self.backend in ["dnf"]

        # expected journal messages from enabling/disabling auto upgrade services
        self.allow_journal_messages("(Created symlink|Removed).*dnf-automatic-install.timer.*")

    def testBasic(self):
        b = self.browser
        m = self.machine

        m.start_cockpit()
        b.login_and_go("/updates")

        if not self.supported_backend:
            self.assertFalse(b.is_present("#automatic"))
            self.assertFalse(b.is_present("#auto-update-type"))
            return

        def wait_pending():
            # the controls get disabled while applying the config changes is in progress; wait for both transitions, to
            # get Phantom checkpoints while the setConfig() promise is running
            b.wait_present("#automatic .disabled")
            b.wait_not_present("#automatic .disabled")

        def assertTimerDnf(time, dow):
            out = m.execute("systemctl --no-legend list-timers dnf-automatic-install.timer")
            if time:
                self.assertIn(" %s:00 " % time, out)
            else:
                self.assertEqual(out, "")
            if dow:
                self.assertRegexpMatches(out, "^%s\s" % dow)
            else:
                # "every day" should not have a "LEFT" time > 1 day
                self.assertNotIn(" day", out)

            # service should not run right away
            self.assertEqual(m.execute("systemctl is-active dnf-automatic-install.service || true").strip(), "inactive")

            # automatic reboots should be enabled whenever timer is enabled
            out = m.execute("systemctl cat dnf-automatic-install.service")
            if time:
                self.assertRegexpMatches(out, "ExecStartPost=/.*shutdown")
            else:
                self.assertNotIn("ExecStartPost", out)

        def assertTimer(time, dow=None):
            if self.backend == "dnf":
                assertTimerDnf(time, dow)
            else:
                raise NotImplementedError(self.backend)

        def assertTypeDnf(_type):
            if _type == "all":
                match = '= default'
            elif _type == "security":
                match = '= security'
            else:
                raise ValueError(_type)

            self.assertIn(match, m.execute("grep upgrade_type /etc/dnf/automatic.conf"))

        def assertType(_type):
            if self.backend == "dnf":
                assertTypeDnf(_type)
            else:
                raise NotImplementedError(self.backend)

        # automatic updates are supported, but off
        b.wait_present("#automatic h2")
        b.wait_present("#automatic div.btn-onoff-ct label.active")
        b.wait_in_text("#automatic div.btn-onoff-ct label.active", "Off")
        self.assertFalse(b.is_present("#auto-update-type"))
        assertTimer(None)

        # enable
        b.click("#automatic div.btn-onoff-ct label.active span")
        wait_pending()
        b.wait_in_text("#automatic div.btn-onoff-ct label.active", "On")
        assertTimer("06:00")
        assertType("all")
        b.wait_present("#auto-update-type button")
        b.wait_in_text("#auto-update-type button", "Apply all updates")
        b.wait_present("#auto-update-time button")
        b.wait_in_text("#auto-update-time button", "06:00")
        b.wait_present("#auto-update-day button")
        b.wait_in_text("#auto-update-day button", "every day")

        # change type to security
        b.click("#auto-update-type button")
        b.wait_present("#auto-update-type .dropdown-menu li[data-data='security'] a")
        b.click("#auto-update-type .dropdown-menu li[data-data='security'] a")
        wait_pending()
        b.wait_in_text("#auto-update-type button", "Apply security updates")
        assertType("security")
        assertTimer("06:00")

        # change it back
        b.click("#auto-update-type button")
        b.wait_present("#auto-update-type .dropdown-menu li[data-data='all'] a")
        b.click("#auto-update-type .dropdown-menu li[data-data='all'] a")
        wait_pending()
        b.wait_in_text("#auto-update-type button", "Apply all updates")
        assertType("all")
        assertTimer("06:00")

        # change day
        b.click("#auto-update-day button")
        b.wait_present("#auto-update-day .dropdown-menu li[data-data='thu'] a")
        b.click("#auto-update-day .dropdown-menu li[data-data='thu'] a")
        wait_pending()
        b.wait_in_text("#auto-update-day button", "Thursday")
        assertType("all")
        assertTimer("06:00", "Thu")

        # change time
        b.click("#auto-update-time button")
        b.wait_present("#auto-update-time .dropdown-menu li[data-data='21:00'] a")
        b.click("#auto-update-time .dropdown-menu li[data-data='21:00'] a")
        wait_pending()
        b.wait_in_text("#auto-update-time button", "21:00")
        assertType("all")
        assertTimer("21:00", "Thu")

        # change back to daily
        b.click("#auto-update-day button")
        b.wait_present("#auto-update-day .dropdown-menu li[data-data=''] a")
        b.click("#auto-update-day .dropdown-menu li[data-data=''] a")
        wait_pending()
        b.wait_in_text("#auto-update-day button", "every day")
        assertType("all")
        assertTimer("21:00")

        # disable
        b.click("#automatic div.btn-onoff-ct label.active span")
        wait_pending()
        b.wait_in_text("#automatic div.btn-onoff-ct label.active", "Off")
        self.assertFalse(b.is_present("#auto-update-type"))
        assertTimer(None)

    def testWithAvailableUpdates(self):
        b = self.browser
        m = self.machine

        self.createPackage("vanilla", "1.0", "1", install=True)
        self.createPackage("vanilla", "1.0", "2")
        self.enableRepo()

        m.start_cockpit()
        b.login_and_go("/updates")
        b.wait_present("#available")

        if not self.supported_backend:
            self.assertFalse(b.is_present("#automatic"))
            self.assertFalse(b.is_present("#auto-update-type"))
            return

        b.wait_present("#automatic")
        b.wait_present("#automatic div.btn-onoff-ct label.active")
        b.wait_in_text("#automatic div.btn-onoff-ct label.active", "Off")
        self.assertFalse(b.is_present("#auto-update-type"))

        # enable
        b.click("#automatic div.btn-onoff-ct label.active span")
        b.wait_in_text("#automatic div.btn-onoff-ct label.active", "On")
        b.wait_present("#auto-update-type")

        if self.backend == 'dnf':
            self.checkUpgradeRebootDnf()
        else:
            raise NotImplementedError(self.backend)

    def checkUpgradeRebootDnf(self):
        """part of testWithAvailableUpdates() for dnf backend"""

        m = self.machine

        # dial down the random sleep to avoid the test having to wait 5 mins
        m.execute("sed -i '/random_sleep/ s/=.*$/= 3/' /etc/dnf/automatic.conf")
        # then manually start the upgrade job like the timer would
        m.execute("systemctl start dnf-automatic-install.service")
        # new vanilla package got installed, and triggered reboot; cancel that
        m.execute("set -e; test -f /stamp-vanilla-1.0-2; test -f /run/nologin; shutdown -c; test ! -f /run/nologin")
        # service should show vanilla upgrade and scheduling shutdown
        out = m.execute("if systemctl status dnf-automatic-install.service; then echo 'expected service to be stopped'; exit 1; fi")
        self.assertIn("vanilla", out)
        self.assertIn("Shutdown scheduled", out)

        # run it again, now there are no available updates → no reboot
        m.execute("systemctl start dnf-automatic-install.service")
        m.execute("set -e; test -f /stamp-vanilla-1.0-2; test ! -f /run/nologin")
        # service should not do much
        out = m.execute("if systemctl status dnf-automatic-install.service; then echo 'expected service to be stopped'; exit 1; fi")
        self.assertNotIn("vanilla", out)
        self.assertNotIn("Shutdown", out)

    def testUnsupported(self):
        b = self.browser
        m = self.machine

        m.execute("if type dnf; then dnf remove -y dnf-automatic; elif type apt; then dpkg -P unattended-upgrades; fi")

        # first test with available upgrades
        self.createPackage("vanilla", "1.0", "1", install=True)
        self.createPackage("vanilla", "1.0", "2")
        self.enableRepo()

        m.start_cockpit()
        b.login_and_go("/updates")

        # automatic upgrades are not visible
        b.wait_present("#available")
        self.assertFalse(b.is_present("#automatic"))

        # apply updates
        b.click("#app .container-fluid button")

        # empty state visible in main area
        b.wait_present(".container-fluid div.blank-slate-pf")
        self.assertFalse(b.is_present("#available"))
        # automatic updates are not visible
        self.assertFalse(b.is_present("#automatic"))


if __name__ == '__main__':
    test_main()
