#!/usr/bin/python # -*- coding: utf-8 -*- # This file is part of Cockpit. # # Copyright (C) 2013 Red Hat, Inc. # # Cockpit is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 2.1 of the License, or # (at your option) any later version. # # Cockpit is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with Cockpit; If not, see . import json import parent from testlib import * class DashBoardHelpers: def check_discovered_addresses(self, b, addresses): b.click('#dashboard-add') b.wait_popup('dashboard_setup_server_dialog') self.wait_discovered_addresses (b, addresses) b.click('#dashboard_setup_server_dialog .btn-default') b.wait_popdown('dashboard_setup_server_dialog') def wait_discovered_addresses(self, b, expected): b.wait_js_func( """(function(expected) { var nodes = document.querySelectorAll('#dashboard_setup_server_dialog datalist option'); var actual = Array.prototype.map.call(nodes, function(e) { return e.getAttribute("value"); }); return expected.sort().toString() == actual.sort().toString(); })""", expected) def wait_dashboard_addresses(self, b, expected): b.wait_js_func( """(function(expected) { var nodes = document.querySelectorAll('#dashboard-hosts .list-group-item'); var addresses = Array.prototype.map.call(nodes, function(e) { return e.getAttribute("data-address"); }); return expected.sort().toString() == addresses.sort().toString(); })""", expected) def machine_remove(self, b, address): b.click("#dashboard-enable-edit") b.click("#dashboard-hosts a[data-address='%s'] button.pficon-delete" % address) def add_machine(self, b, address, known_host=False): b.click('#dashboard-add') b.wait_popup('dashboard_setup_server_dialog') b.set_val('#add-machine-address', address) self.add_machine_finish(b, known_host=known_host) def add_machine_finish(self, b, known_host=False): b.wait_text('#dashboard_setup_server_dialog .btn-primary', "Add") b.wait_present("#dashboard_setup_server_dialog .btn-primary:not([disabled])") b.click('#dashboard_setup_server_dialog .btn-primary') if not known_host: b.wait_in_text('#dashboard_setup_server_dialog', "Fingerprint") b.click('#dashboard_setup_server_dialog .btn-primary') b.wait_popdown('dashboard_setup_server_dialog') @skipImage("No dashboard on Atomic", "continuous-atomic", "fedora-atomic", "rhel-atomic") class TestBasicDashboard(MachineCase, DashBoardHelpers): additional_machines = { 'machine2': { }, 'machine3': { } } def testBasic(self): b = self.browser m = self.machine m2 = self.machines['machine2'] m3 = self.machines['machine3'] self.login_and_go("/dashboard") self.wait_dashboard_addresses (b, [ "localhost" ]) # Start second browser and check that it is in sync b2 = self.new_browser() b2.default_user = "root" b2.login_and_go("/dashboard") self.wait_dashboard_addresses (b2, [ "localhost" ]) b.wait_present("#dashboard-hosts a[data-address='localhost'] button.pficon-delete.disabled") self.add_machine (b, m2.address) self.wait_dashboard_addresses (b, [ "localhost", m2.address ]) self.wait_dashboard_addresses (b2, [ "localhost", m2.address ]) # Should show successful state and icon b.wait_present("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address) b.wait_attr("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address, 'src', '../shell/images/server-small.png') b2.wait_present("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address) b2.wait_attr("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address, 'src', '../shell/images/server-small.png') self.add_machine (b, m3.address) self.wait_dashboard_addresses (b, [ "localhost", m3.address, m2.address ]) self.wait_dashboard_addresses (b2, [ "localhost", m3.address, m2.address ]) # Remove two self.machine_remove (b, m2.address) self.wait_dashboard_addresses (b, [ "localhost", m3.address ]) self.wait_dashboard_addresses (b2, [ "localhost", m3.address ]) self.machine_remove (b, m3.address) self.wait_dashboard_addresses (b, [ "localhost" ]) self.wait_dashboard_addresses (b2, [ "localhost" ]) # Check that the two removed machines are listed in "Add Host" # on both browsers self.check_discovered_addresses (b, [ m2.address, m3.address ]) self.check_discovered_addresses (b2, [ m2.address, m3.address ]) # Add one back, check addresses on both browsers self.add_machine (b, m2.address, True) self.wait_dashboard_addresses (b, [ "localhost", m2.address ]) self.wait_dashboard_addresses (b2, [ "localhost", m2.address ]) self.check_discovered_addresses (b, [ m3.address ]) self.check_discovered_addresses (b2, [ m3.address ]) # Should show successful state and icon again b.wait_present("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address) b.wait_attr("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address, 'src', '../shell/images/server-small.png') b2.wait_present("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address) b2.wait_attr("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address, 'src', '../shell/images/server-small.png') # And the second one, check addresses on both browsers self.add_machine (b, m3.address, True) self.wait_dashboard_addresses (b, [ "localhost", m2.address, m3.address ]) self.wait_dashboard_addresses (b2, [ "localhost", m2.address, m3.address ]) self.check_discovered_addresses (b, [ ]) self.check_discovered_addresses (b2, [ ]) # Move m2 known host key to ~/.ssh, verify that it's known self.machine_remove (b, m2.address) self.wait_dashboard_addresses (b, [ "localhost", m3.address ]) key = m.execute("grep '{0}' /etc/ssh/ssh_known_hosts && sed -i '/{0}/d' /etc/ssh/ssh_known_hosts".format(m2.address)) m.execute("mkdir ~admin/.ssh && echo '{0}' > ~admin/.ssh/known_hosts && chown -R admin:admin ~admin/.ssh""".format(key)) self.add_machine (b, m2.address, True) self.wait_dashboard_addresses (b, [ "localhost", m2.address, m3.address ]) # Should be connected, no error b.wait_present("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address) b.wait_attr("#dashboard-hosts a.connected[data-address='%s'] img" % m2.address, 'src', '../shell/images/server-small.png') # Test change user, not doing in edit to reuse machines # Navigate to load iframe b.click("#dashboard-hosts .list-group-item[data-address='{0}']".format(m3.address)) b.enter_page("/system", m3.address); b.switch_to_top() b.go("/dashboard") b.enter_page("/dashboard") b.switch_to_top() b.wait_present("iframe.container-frame[name='cockpit1:{0}/system']".format(m3.address)) b.enter_page("/dashboard") b.click('#dashboard-enable-edit') b.wait_visible("#dashboard-hosts .list-group-item[data-address='{0}'] button.pficon-edit".format(m3.address)) b.click("#dashboard-hosts .list-group-item[data-address='{0}'] button.pficon-edit".format(m3.address)) b.wait_popup('host-edit-dialog') b.wait_visible('#host-edit-user') b.set_val('#host-edit-user', 'bad-user') b.click('#host-edit-apply') b.wait_popdown('host-edit-dialog') b.wait_not_present("iframe.container-frame[name='cockpit1:{0}/system']".format(m3.address)) b.wait_present("#dashboard-hosts .failed[data-address='{0}']".format(m3.address)) b.click('#dashboard-enable-edit') b.wait_visible("#dashboard-hosts .list-group-item[data-address='{0}'] button.pficon-edit".format(m3.address)) b.click("#dashboard-hosts .list-group-item[data-address='{0}'] button.pficon-edit".format(m3.address)) b.wait_popup('host-edit-dialog') b.wait_visible('#host-edit-user') b.set_val('#host-edit-user', '') b.click('#host-edit-apply') b.wait_popdown('host-edit-dialog') b.wait_present("#dashboard-hosts .connected[data-address='{0}']".format(m3.address)) # Test switching b.leave_page() b.wait_js_cond('$("#machine-dropdown ul li").length == 3') b.click("#machine-link") b.click("#machine-dropdown ul a[data-address='localhost']") b.wait_js_cond('window.location.pathname == "/system"') b.click("#machine-link") b.click("#machine-dropdown ul a[data-address='%s']" % m2.address) b.wait_js_cond('window.location.pathname.indexOf("/@%s") === 0' % m2.address) b.click("#machine-link") b.click("#machine-dropdown ul a[data-address='%s']" % m3.address) b.wait_js_cond('window.location.pathname.indexOf("/@%s") === 0' % m3.address) self.allow_hostkey_messages() self.allow_journal_messages(".*server offered unsupported authentication methods: password public-key.*") @skipImage("No dashboard on Atomic", "continuous-atomic", "fedora-atomic", "rhel-atomic") class TestDashboardSetup(MachineCase, DashBoardHelpers): additional_machines = { 'machine2': { } } def testSetup(self): b = self.browser m1 = self.machine m2 = self.machines['machine2'] # lockout admin m2.execute("echo admin:badpass | chpasswd") # Create some users on m1 and m2. m1.execute("getent group docker >/dev/null || groupadd docker") m2.execute("getent group docker >/dev/null || groupadd docker") m1.execute("useradd junior -G docker") m1.execute("echo junior:foobar | chpasswd") m1.execute("useradd nosync -G docker") m1.execute("echo nosync:foobar | chpasswd") m1.execute("useradd senior -G %s" % m1.get_admin_group()) m1.execute("echo senior:foobar | chpasswd") m2.execute("useradd senior") m2.execute("echo senior:barfoo | chpasswd") # Sync them via Setup. self.login_and_go("/dashboard") self.wait_dashboard_addresses (b, [ "localhost" ]) b.click('#dashboard-add') b.wait_popup('dashboard_setup_server_dialog') b.set_val('#add-machine-address', m2.address) b.wait_text('#dashboard_setup_server_dialog .btn-primary', "Add") b.wait_present("#dashboard_setup_server_dialog .btn-primary:not([disabled])") b.click('#dashboard_setup_server_dialog .btn-primary') b.wait_in_text('#dashboard_setup_server_dialog', "Fingerprint") b.click('#dashboard_setup_server_dialog .btn-primary') b.wait_in_text('#dashboard_setup_server_dialog .modal-title', "Log in to".format(m2.address)) b.wait_present("#do-sync-users") b.click("#do-sync-users") b.wait_text('#dashboard_setup_server_dialog .btn-primary', "Synchronize") b.wait_present("#sync-users") b.wait_in_text("#sync-users", "admin") b.wait_present("#sync-username") b.click('#sync-users input[name="admin"]') b.click('#sync-users input[name="junior"]') b.click('#sync-users input[name="senior"]') b.set_val('#sync-username', "root") b.set_val('#sync-password', "notthepassword") b.click('#dashboard_setup_server_dialog .btn-primary') b.wait_present("#dashboard_setup_server_dialog .btn-primary:not([disabled])") b.wait_text('#dashboard_setup_server_dialog .dialog-error', "Login failed") b.wait_present('#sync-password') b.set_val('#sync-password', "foobar") b.click('#dashboard_setup_server_dialog .btn-primary') b.wait_popdown('dashboard_setup_server_dialog') # Check the result def password_hash(machine, user): return machine.execute("getent shadow %s | cut -d: -f2" % user) def groups(machine, user): return machine.execute("groups %s | cut -d: -f2" % user) self.assertEqual(password_hash(m1, "junior"), password_hash(m2, "junior")) self.assertEqual(password_hash(m1, "senior"), password_hash(m2, "senior")) self.assertEqual(password_hash(m1, "admin"), password_hash(m2, "admin")) self.assertIn ("docker", groups(m2, "junior")) self.assertIn (m2.get_admin_group(), groups(m2, "senior")) try: m2.execute("id nosync") self.assertFalse("User nosync should not have been synced") except: pass m2.execute("ps aux | grep cockpit-bridge | grep admin") self.allow_hostkey_messages() def testMachinesJsonVarMigration(self): b = self.browser m = self.machine # create obsolete machines.json in /var blue_conf = '{"blue": {"address": "1.2.3.4", "visible": true}}' m.execute("echo '%s' > /var/lib/cockpit/machines.json" % blue_conf) # prevent migration due to permission error; should not crash and keep/respect the old file m.execute("""chattr +i /etc/cockpit/machines.d""") self.login_and_go("/dashboard") wait(lambda: m.execute("journalctl -b | grep 'migration.*machines.json.*failed'")) self.wait_dashboard_addresses (b, [ "localhost", "1.2.3.4" ]) b.logout() # should not have written anything and left the original file untouched m.execute('chattr -i /etc/cockpit/machines.d && [ "$(ls /etc/cockpit/machines.d)" = "" ]') self.assertEqual(m.execute("cat /var/lib/cockpit/machines.json").strip(), blue_conf) self.allow_journal_messages(".*migration of /var/lib/cockpit/machines.json to /etc/cockpit/machines.d/99-webui.json failed:.*") # now machines.d/ is writable, should migrate self.login_and_go("/dashboard") wait(lambda: m.execute("test ! -e /var/lib/cockpit/machines.json && ls /etc/cockpit/machines.d/99-webui.json")) self.wait_dashboard_addresses (b, [ "localhost", "1.2.3.4" ]) b.logout() self.assertEqual(m.execute('cat /etc/cockpit/machines.d/99-webui.json').strip(), blue_conf) # old /var file should not clobber existing 99-webui.json but move to 98-migrated.json and merge its data green_conf = '{"green": {"address": "9.8.7.6", "visible": true}}' m.execute("echo '%s' > /var/lib/cockpit/machines.json" % green_conf) self.login_and_go("/dashboard") wait(lambda: m.execute("test ! -e /var/lib/cockpit/machines.json && ls /etc/cockpit/machines.d/98-migrated.json")) self.wait_dashboard_addresses (b, [ "localhost", "1.2.3.4", "9.8.7.6" ]) b.logout() self.assertEqual(m.execute('cat /etc/cockpit/machines.d/99-webui.json').strip(), blue_conf) self.assertEqual(m.execute('cat /etc/cockpit/machines.d/98-migrated.json').strip(), green_conf) self.allow_journal_messages('.* couldn\'t connect: .*') @skipImage("No dashboard on Atomic", "continuous-atomic", "fedora-atomic", "rhel-atomic") class TestDashboardEditLimits(MachineCase, DashBoardHelpers): def testEdit(self): b = self.browser m = self.machine self.login_and_go("/dashboard") self.wait_dashboard_addresses (b, [ "localhost" ]) old_style = b.attr("#dashboard-hosts .list-group-item[data-address='localhost']", "style") b.wait_not_visible('#dashboard-hosts a:first-child button.pficon-edit') b.click('#dashboard-enable-edit') b.wait_visible('#dashboard-hosts a:first-child button.pficon-edit') b.click('#dashboard-hosts a:first-child button.pficon-edit') b.wait_not_visible('#dashboard-hosts a:first-child button.pficon-edit') b.wait_popup('host-edit-dialog') b.set_val('#host-edit-name', "Horst") b.click('#host-edit-color') b.wait_visible('#host-edit-color-popover') b.wait_visible('#host-edit-user[disabled]') b.click('#host-edit-color-popover div.popover-content > div:first-child > div:nth-child(3)') b.wait_not_visible('#host-edit-color-popover') b.click('#host-edit-apply') b.wait_popdown('host-edit-dialog') b.wait_in_text('#dashboard-hosts a:first-child span.host-label', "Horst") b.wait_not_attr("#dashboard-hosts .list-group-item[data-address='localhost']", "style", old_style) self.assertEqual(m.execute("hostnamectl --pretty"), "Horst\n") def testLimits(self): b = self.browser m = self.machine def fake_machines(amount): # build a machine json manually d = { "localhost" : {"visible":True,"address":"localhost"} } for i in range(amount): n = "bad{0}".format(i) d[n] = {"visible":True,"address":n} m.execute("echo '{0}' > /etc/cockpit/machines.d/99-webui.json".format(json.dumps(d))) return d.keys() def check_limit(limit): b.click('#dashboard-add') b.wait_popup('dashboard_setup_server_dialog') if limit: b.wait_visible("#dashboard_setup_server_dialog .dashboard-machine-warning") b.wait_in_text("#dashboard_setup_server_dialog .dashboard-machine-warning", "{0} machines".format(limit)) else: b.wait_not_present("#dashboard_setup_server_dialog .dashboard-machine-warning") b.click("#dashboard_setup_server_dialog .btn-default") self.login_and_go("/dashboard") self.wait_dashboard_addresses (b, [ "localhost" ]) check_limit(0) self.wait_dashboard_addresses (b, fake_machines(3)) check_limit(0) self.wait_dashboard_addresses (b, fake_machines(14)) check_limit(20) self.allow_journal_messages(".*couldn't connect: Failed to resolve hostname bad.*") if __name__ == '__main__': test_main()