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

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

import parent
from testlib import *
import json
import os
import sys
import unittest

class TestDocker(MachineCase):
    def confirm(self):
        b = self.browser
        b.wait_popup("confirmation-dialog")
        b.click("#confirmation-dialog-confirm")
        # popdown should be really quick
        with b.wait_timeout(5):
            b.wait_popdown("confirmation-dialog")

    def del_container_from_details(self, container_name):
        b = self.browser
        b.wait_visible("#container-details-delete:not([disabled])")
        b.click("#container-details-delete")
        with b.wait_timeout(20):
            try:
                self.confirm()
                b.wait_visible("#containers")
                b.wait_not_in_text("#containers-containers", container_name)
            except:
                # we may have to confirm again for forced deletion
                self.confirm()
                b.wait_visible("#containers")
                b.wait_not_in_text("#containers-containers", container_name)

    def wait_for_message(self, message):
        b = self.browser
        # sometimes container output is missing from the logs
        # https://github.com/docker/docker/issues/10617
        b.wait_in_text("#container-terminal", message)

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

        m.execute("systemctl start docker")

        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        message = "HelloMessage."

        # show all containers to interact with stopped ones
        b.click("#containers-containers-filter button")
        b.wait_visible("#containers-containers-filter .dropdown-menu")
        b.click("#containers-containers-filter a[value='all']")

        # Run it
        b.click('#containers-images tr:contains("busybox:latest") button.fa-play')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE1")
        b.set_val("#containers-run-image-command", "/bin/echo '%s'" % (message))
        b.click("#containers-run-image-run");
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE1")

        # Check output of the probe
        b.click('#containers-containers tr:contains("PROBE1")')
        b.wait_visible('#container-details')
        # The terminal uses NO-BREAK SPACE so we check for that here
        self.wait_for_message(message)
        b.wait_in_text("#container-details-state", "Exited")

        self.del_container_from_details("PROBE1")

        # Run it without TTY and make sure it keeps running so that we
        # can link to it.
        b.click('#containers-images tr:contains("busybox:latest") button.fa-play')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE1")
        b.set_val("#containers-run-image-command", "/bin/sh -c 'echo \"%s\"; sleep 10000'" % (message))
        b.click("#containers-run-image-with-terminal");
        b.click("#containers-run-image-run");
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE1")

        # Check not linked
        info = json.loads(m.execute('docker inspect PROBE1'))
        self.assertEqual(info[0]["HostConfig"]["Links"], None)

        # Create another container linked to old one
        b.click('#containers-images tr:contains("busybox:latest") button.fa-play')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE2")
        b.set_val("#containers-run-image-command", "/bin/echo '%s'" % (message))
        b.click("#containers-run-image-with-terminal");
        b.wait_visible("#select-linked-containers:empty");
        b.click("#link-containers");
        b.wait_visible("#select-linked-containers:parent");
        b.click("#select-linked-containers form:last-child .link-container button")
        b.wait_visible("#select-linked-containers form:last-child .link-container a[value='PROBE1']")
        b.click("#select-linked-containers form:last-child .link-container a[value='PROBE1']")
        b.set_val("#select-linked-containers form:last-child  input[name='alias']", "alias1")
        b.click("#select-linked-containers form:last-child  button.fa-plus");
        b.click("#select-linked-containers form:last-child .link-container button")
        b.wait_visible("#select-linked-containers form:last-child .link-container a[value='PROBE1']")
        b.click("#select-linked-containers form:last-child .link-container a[value='PROBE1']")
        b.set_val("#select-linked-containers form:last-child  input[name='alias']", "alias2")
        b.click("#containers-run-image-run");
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE2")

        # Wait for PROBE2 to Exit
        b.wait_present('#containers-containers tr:contains("PROBE2")')
        b.wait_visible('#containers-containers tr:contains("PROBE2")')
        b.click('#containers-containers tr:contains("PROBE2")')
        b.wait_visible("#container-details")
        b.wait_in_text("#container-details-state", "Exited")

        # Check links
        info = json.loads(m.execute('docker inspect PROBE2'))
        self.assertEqual(set(info[0]["HostConfig"]["Links"]),
                         set(["/PROBE1:/PROBE2/alias1", "/PROBE1:/PROBE2/alias2"]))

        # delete PROBE2 from dash
        b.click("#container-details ol.breadcrumb a")
        b.wait_visible("#containers-containers")
        b.wait_in_text('#containers-containers tbody', "PROBE2")
        b.wait_not_visible("#containers-containers tbody tr:contains('PROBE2') button.btn-delete")
        b.click("#containers-containers button.enable-danger")
        b.wait_visible("#containers-containers tbody tr:contains('PROBE2') button.btn-delete")
        b.wait_not_present("#containers-containers tbody tr:contains('PROBE2') button.btn-delete.disabled")
        b.click("#containers-containers tbody tr:contains('PROBE2') button.btn-delete")
        with b.wait_timeout(20):
            try:
                b.wait_not_in_text('#containers-containers tbody', "PROBE2")
            except:
                self.confirm()
                b.wait_not_in_text('#containers-containers tbody', "PROBE2")

        # Check output of PROBE1
        b.click('#containers-containers tr:contains("PROBE1")')
        b.wait_visible("#container-details")
        self.wait_for_message(message)
        b.call_js_func("ph_count_check", ".console-ct pre",  1)
        b.click("#container-details-stop")
        b.wait_in_text("#container-details-state", "Exited")

        # Make sure after restart we only have one
        b.wait_visible("#container-details-start")
        b.click("#container-details-start")
        self.wait_for_message("%s\n%s" % (message, message))
        b.call_js_func("ph_count_check", ".console-ct pre",  1)
        b.wait_present("#container-details-stop:not([disabled])")
        b.click("#container-details-stop")
        b.wait_in_text("#container-details-state", "Exited")

        self.del_container_from_details("PROBE1")

        # Delete image
        b.click('#containers-images tr:contains("busybox:latest")')
        b.wait_visible("#image-details")
        b.click("#image-details-delete")
        self.confirm()
        b.wait_visible("#containers")
        b.wait_not_in_text('#containers-images', 'busybox:latest')

    def testExpose(self):
        b = self.browser
        m = self.machine
        self.allow_journal_messages('.*denied.*name_connect.*docker.*')

        m.execute("systemctl start docker")

        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        port = 3380;
        message = "HelloMessage."

        # Create an updated image, expose a port
        m.execute("mkdir /var/tmp/container-probe")
        m.upload(["verify/files/listen-on-port.sh"], "/var/tmp/container-probe")
        m.execute("""echo -e '
FROM busybox
MAINTAINER cockpit
EXPOSE %(port)d
ADD listen-on-port.sh /listen-on-port.sh
CMD ["/listen-on-port.sh", "%(port)d", "%(message)s"]
' > /var/tmp/container-probe/Dockerfile""" % {'message': message, 'port': port})
        image_name = "test/container-probe"
        m.execute("docker build -t %s /var/tmp/container-probe" % (image_name))
        m.execute("rm -rf /var/tmp/container-probe")

        # Wait for it to appear
        b.wait_in_text("#containers-images", image_name)

        nport = port + 1;

        # Run it and expose additional port via the ui
        b.click("#containers-images tr:contains(\"%s\") button.fa-play" % (image_name))
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE2")
        # Tell our probe to listen on both ports
        second_message = "%s" % (message);
        b.set_val("#containers-run-image-command",
                  ("/bin/sh -c \"/listen-on-port.sh %(port)d %(message)s; " +
                               "/listen-on-port.sh %(second_port)d %(second_message)s\"")
                    % {'port': port,
                       'message': message,
                       'second_port': nport,
                       'second_message': second_message})
        b.set_val(".containers-run-portmapping form:eq(0) input:eq(1)", port)
        b.click(".containers-run-portmapping form:eq(0) .fa-plus")
        b.set_val(".containers-run-portmapping form:eq(1) input:eq(0)", nport)
        b.set_val(".containers-run-portmapping form:eq(1) input:eq(1)", nport)

        b.click("#containers-run-image-run");
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE2")

        # Check output of the probe
        b.wait_present('#containers-containers tr:contains("PROBE2")')
        b.wait_visible('#containers-containers tr:contains("PROBE2")')
        b.click('#containers-containers tr:contains("PROBE2")')
        b.wait_visible("#container-details")
        b.wait_in_text("#container-details-ports", ":%d -> %d/tcp" % (port, port))
        b.wait_in_text("#container-details-ports", ":%d -> %d/tcp" % (nport, nport))
        self.wait_for_message(message)

        # Check connection on first port, remove trailing whitespace
        data = m.execute("exec 3<>/dev/tcp/localhost/%d; cat <&3" % (port)).rstrip()
        self.assertEqual(data, message)

        self.wait_for_message(second_message)

        # Check connection on second port, remove trailing whitespace
        data = m.execute("exec 3<>/dev/tcp/localhost/%d; cat <&3" % (nport)).rstrip()
        self.assertEqual(data, second_message)

        # Wait for exit
        b.wait_in_text("#container-details-state", "Exited")

        self.del_container_from_details("PROBE2")

    @unittest.skipIf("atomic" in os.environ.get("TEST_OS", ""), "we can't use cockpit on atomic if docker is stopped")
    def testFailure(self):
        b = self.browser
        m = self.machine

        # Try to access docker without admin (wheel, sudo, ...) group
        m.execute("systemctl start docker")
        m.execute("usermod -G '' admin")

        # HACK: Logging in without the admin group will try to write to
        # /var/lib/cockpit/, but this will fail.
        # https://github.com/cockpit-project/cockpit/issues/1248
        #
        self.allow_journal_messages(".*Failed to create file '/var/lib/cockpit/.*': Permission denied")
        self.allow_journal_messages("http:///var/run/docker.sock/.*: couldn't connect: .*")

        self.login_and_go("/docker")

        # We can not become root, so we can't access docker.
        b.wait_visible("#curtain")
        b.wait_text("#curtain h1", "Not authorized to access Docker on this system")
        b.wait_visible("#curtain button[data-action='docker-connect']")

        # Give "admin" access via the admin group and login again
        m.execute("usermod -G %s admin" % m.get_admin_group())
        m.execute("systemctl stop docker")
        b.relogin("/docker")
        self.allow_restart_journal_messages()

        # Now we can become root and start docker
        b.wait_visible("#curtain")
        b.wait_text("#curtain h1", "Docker is not installed or activated on the system")
        b.click("#curtain button[data-action='docker-start']")
        b.wait_visible("#containers-containers")

        self.allow_authorize_journal_messages()
        self.allow_journal_messages("cannot reauthorize identity.*:.*unix-user:root.*")

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

        m.execute("systemctl start docker")

        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        # Run one container without restarting
        b.click('#containers-images tr:contains("busybox:latest") button.fa-play')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "NORESTART")
        b.set_val("#containers-run-image-command", "/bin/sh -c 'echo \"no-restart\"; sleep 10000'")
        b.click("#containers-run-image-with-terminal");
        b.click("#containers-run-image-run");
        b.wait_popdown("containers_run_image_dialog")

        # Run another container with restarting
        b.click('#containers-images tr:contains("busybox:latest") button.fa-play')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "YAYRESTART")
        b.set_val("#containers-run-image-command", "/bin/sh -c 'echo \"yay-restart\"; sleep 10000'")
        b.click("#containers-run-image-with-terminal");
        b.click("#restart-policy-select button");
        b.click("#restart-policy-select a[data-value=always]");
        b.wait_in_text("#restart-policy-select button", "Always");
        b.click("#containers-run-image-run");
        b.wait_popdown("containers_run_image_dialog")

        # Make sure they've started
        b.wait_in_text("#containers-containers", "NORESTART")
        b.wait_in_text("#containers-containers", "YAYRESTART")

        # Now restart docker
        b.logout()
        m.execute("systemctl restart docker")
        self.login_and_go("/docker")

        # Now check that one restarts
        b.wait_in_text("#containers-images", "busybox:latest")
        b.wait_present("#containers-containers tr:contains(\"YAYRESTART\")")
        b.wait_visible("#containers-containers tr:contains(\"YAYRESTART\")")
        b.wait_present("#containers-containers tr:contains(\"NORESTART\")")
        b.wait_not_visible("#containers-containers tr:contains(\"NORESTART\")")


if __name__ == '__main__':
    test_main()
