From e4aa77d9f2ee938acdda27481873135319d21197 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 6 Nov 2017 13:10:24 +0530 Subject: [PATCH] cockpit: Make the application usable All users can login. Only admin users can see logs and make changes. LIMITATION: Only certain functions such as service management is possible. Functions such as networking and user management is read-only. This problem does not occur for user belonging to the 'sudo' group. - Move to system section from applications section. - Rename action script to cockpit instead of cockpit. - Deal with .socket/.service correctly. - Implement hooks on domain name changes and update configuration correctly. - Host the application under /_cockpit instead of /cockpit because it is reserved. - Update description. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- actions/cockpit | 139 ++++++++++++++++++ actions/cockpit.socket | 110 -------------- .../conf-available/cockpit-freedombox.conf | 16 ++ .../conf-available/cockpit-plinth.conf | 21 --- plinth/modules/cockpit/__init__.py | 80 ++++++---- plinth/modules/cockpit/urls.py | 15 +- 6 files changed, 211 insertions(+), 170 deletions(-) create mode 100755 actions/cockpit delete mode 100755 actions/cockpit.socket create mode 100644 data/etc/apache2/conf-available/cockpit-freedombox.conf delete mode 100644 data/etc/apache2/conf-available/cockpit-plinth.conf diff --git a/actions/cockpit b/actions/cockpit new file mode 100755 index 000000000..5e2e79015 --- /dev/null +++ b/actions/cockpit @@ -0,0 +1,139 @@ +#!/usr/bin/python3 +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Configuration helper for Cockpit. +""" + +import argparse +import augeas + +from plinth import action_utils + +CONFIG_FILE = '/etc/cockpit/cockpit.conf' + + +def parse_arguments(): + """Return parsed command line arguments as dictionary.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + subparser = subparsers.add_parser( + 'setup', help='Setup Cockpit configuration') + subparser.add_argument( + 'domain_names', nargs='*', help='Domain names to be allowed') + subparsers.add_parser('enable', help='Enable Cockpit') + subparsers.add_parser('disable', help='Disable Cockpit') + subparser = subparsers.add_parser( + 'add-domain', + help='Allow a new domain to be origin for Cockpit\'s WebSocket') + subparser.add_argument('domain_name', help='Domain name to be allowed') + subparser = subparsers.add_parser( + 'remove-domain', + help='Disallow a new domain from being origin for Cockpit\'s ' + 'WebSocket') + subparser.add_argument('domain_name', help='Domain name to be removed') + + subparsers.required = True + return parser.parse_args() + + +def subcommand_setup(arguments): + """Setup Cockpit configuration.""" + aug = load_augeas() + origins = [ + _get_origin_from_domain(domain) for domain in arguments.domain_names + ] + origins += ['https://localhost', 'https://localhost:4430'] + _set_origin_domains(aug, origins) + aug.set('/files' + CONFIG_FILE + '/WebService/UrlRoot', '/_cockpit/') + aug.save() + + with action_utils.WebserverChange() as webserver_change: + webserver_change.enable('proxy_wstunnel', kind='module') + webserver_change.enable('cockpit-freedombox') + + action_utils.service_restart('cockpit.socket') + + +def subcommand_enable(_): + """Enable web configuration and reload.""" + action_utils.service_enable('cockpit.socket') + action_utils.webserver_enable('cockpit-freedombox') + + +def subcommand_disable(_): + """Disable web configuration and reload.""" + action_utils.webserver_disable('cockpit-freedombox') + action_utils.service_disable('cockpit.socket') + + +def _get_origin_domains(aug): + """Return the list of allowed origin domains.""" + origins = aug.get('/files' + CONFIG_FILE + '/WebService/Origins') + return set(origins.split()) + + +def _set_origin_domains(aug, origins): + """Set the list of allowed origin domains.""" + aug.set('/files' + CONFIG_FILE + '/WebService/Origins', ' '.join(origins)) + + +def _get_origin_from_domain(domain): + """Return the origin that should be allowed for a domain.""" + return 'https://{domain}'.format(domain=domain) + + +def subcommand_add_domain(arguments): + """Allow a new domain to be origin for Cockpit's WebSocket.""" + aug = load_augeas() + origins = _get_origin_domains(aug) + origins.add(_get_origin_from_domain(arguments.domain_name)) + _set_origin_domains(aug, origins) + aug.save() + + +def subcommand_remove_domain(arguments): + """Disallow a domain from being origin for Cockpit's WebSocket.""" + aug = load_augeas() + origins = _get_origin_domains(aug) + origins.remove(_get_origin_from_domain(arguments.domain_name)) + _set_origin_domains(aug, origins) + aug.save() + + +def load_augeas(): + """Initialize Augeas.""" + aug = augeas.Augeas( + flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD) + aug.set('/augeas/load/inifile/lens', 'Puppet.lns') + aug.set('/augeas/load/inifile/incl[last() + 1]', CONFIG_FILE) + aug.load() + return aug + + +def main(): + """Parse arguments and perform all duties.""" + arguments = parse_arguments() + + subcommand = arguments.subcommand.replace('-', '_') + subcommand_method = globals()['subcommand_' + subcommand] + subcommand_method(arguments) + + +if __name__ == '__main__': + main() diff --git a/actions/cockpit.socket b/actions/cockpit.socket deleted file mode 100755 index 7fa660d6d..000000000 --- a/actions/cockpit.socket +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/python3 -# -*- mode: python -*- -# -# This file is part of Plinth. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -""" -Configuration helper for Cockpit. -""" - -import argparse -import augeas -import subprocess - -from plinth import action_utils - -CONFIG_FILE = '/etc/cockpit/cockpit.conf' - -def parse_arguments(): - """Return parsed command line arguments as dictionary.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') - - subparsers.add_parser('pre-setup', help='Perform pre-setup operations') - subparsers.add_parser('setup', help='Setup Cockpit configuration') - subparsers.add_parser('enable', help='Enable Cockpit site') - subparsers.add_parser('disable', help='Disable Cockpit site') - - subparsers.required = True - return parser.parse_args() - - -def subcommand_pre_setup(_): - """Preseed debconf values before packages are installed.""" - """subprocess.check_output( - ['debconf-set-selections'], - input=b'cockpit tt-rss/database-type string pgsql')""" - pass - -def subcommand_setup(_): - """Setup Cockpit configuration.""" - - open(CONFIG_FILE, 'a').close() - - aug = load_augeas() - SECTION = CONFIG_FILE+'/WebService' - aug.set('/files'+SECTION, '') - origins = "https://localhost:9090 https://localhost:4430" - aug.set('/files'+SECTION+'/Origins', origins) - aug.save() - - action_utils.webserver_enable('proxy_wstunnel', kind='module') - action_utils.service_restart('cockpit.socket') - action_utils.webserver_enable('cockpit-plinth') - action_utils.service_restart('apache2') - -def subcommand_enable(_): - """Enable web configuration and reload.""" - action_utils.service_enable('cockpit.socket') -# action_utils.webserver_enable('cockpit-plinth') - - -def subcommand_disable(_): - """Disable web configuration and reload.""" -# action_utils.webserver_disable('cockpit-plinth') - action_utils.service_disable('cockpit.socket') - -def subcommand_add_domain(_): - aug = load_augeas() - -def subcommand_remove_domain(_): - aug = load_augeas() - -def subcommand_change_domain(_): - aug = load_augeas() - -def load_augeas(): - """Initialize Augeas.""" - aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + - augeas.Augeas.NO_MODL_AUTOLOAD) - aug.set('/augeas/load/inifile/lens', 'Puppet.lns') - aug.set('/augeas/load/inifile/incl[last() + 1]', CONFIG_FILE) - aug.load() - return aug - - -def main(): - """Parse arguments and perform all duties.""" - arguments = parse_arguments() - - subcommand = arguments.subcommand.replace('-', '_') - subcommand_method = globals()['subcommand_' + subcommand] - subcommand_method(arguments) - - -if __name__ == '__main__': - main() diff --git a/data/etc/apache2/conf-available/cockpit-freedombox.conf b/data/etc/apache2/conf-available/cockpit-freedombox.conf new file mode 100644 index 000000000..001c2537f --- /dev/null +++ b/data/etc/apache2/conf-available/cockpit-freedombox.conf @@ -0,0 +1,16 @@ +## +## On all sites, provide cockpit on the path: /_cockpit/ +## +## Requires the following Apache modules to be enabled: +## mod_headers +## mod_proxy +## mod_proxy_http +## mod_proxy_wstunnel +## + + ProxyPass http://localhost:9090/_cockpit/ + + + + ProxyPass ws://localhost:9090/_cockpit/socket + diff --git a/data/etc/apache2/conf-available/cockpit-plinth.conf b/data/etc/apache2/conf-available/cockpit-plinth.conf deleted file mode 100644 index 9509a0133..000000000 --- a/data/etc/apache2/conf-available/cockpit-plinth.conf +++ /dev/null @@ -1,21 +0,0 @@ -## -## On all sites, provide cockpit on a defaut path: /cockpit -## -## Requires the following Apache modules to be enabled: -## mod_headers -## mod_proxy -## mod_proxy_http -## mod_proxy_wstunnel -## - - - proxypass http://localhost:9090/ - - - - proxypass http://localhost:9090/cockpit/ - - - - proxypass ws://localhost:9090/cockpit/socket - diff --git a/plinth/modules/cockpit/__init__.py b/plinth/modules/cockpit/__init__.py index dd44b8760..b1b32896f 100644 --- a/plinth/modules/cockpit/__init__.py +++ b/plinth/modules/cockpit/__init__.py @@ -28,27 +28,34 @@ from plinth import cfg from plinth import frontpage from plinth import service as service_module from plinth.menu import main_menu +from plinth.modules import names from plinth.signals import domain_added, domain_removed, domainname_change - version = 1 managed_services = ['cockpit.socket'] managed_packages = ['cockpit'] -title = _('Dashboard of Servers \n (Cockpit)') +name = _('Cockpit') + +short_description = _('Server Administration') description = [ - _('Cockpit is an interactive server admin interface. It is easy to use ' - 'and very light weight. Cockpit interacts directly with the operating ' - 'system from a real linux session in a browser'), - format_lazy( - _('When enabled, Cockpit will be available from ' - '/cockpit path on the web server. It can be accessed by ' - 'any user with a {box_name} login.'), - box_name=_(cfg.box_name)) + _('Cockpit is a server manager that makes it easy to administer ' + 'GNU/Linux servers via a web browser. On a {box_name}, controls ' + 'are available for many advanced functions that are not usually ' + 'required. A web based terminal for console operations is also ' + 'available.'), box_name=_(cfg.box_name)), + format_lazy( + _('When enabled, Cockpit will be available from ' + '/_cockpit/ path on the web server. It can be accessed by ' + 'any user with a {box_name} login. ' + 'Sensitive information and system altering abilities are limited to ' + 'users belonging to admin group.'), + box_name=_(cfg.box_name)), + _('Currently only limited functionality is available.'), ] service = None @@ -56,30 +63,37 @@ service = None def init(): """Intialize the module.""" - menu = main_menu.get('apps') - menu.add_urlname(title, 'glyphicon-dashboard', 'cockpit:index') + menu = main_menu.get('system') + menu.add_urlname(name, 'glyphicon-wrench', 'cockpit:index', + short_description) global service setup_helper = globals()['setup_helper'] if setup_helper.get_state() != 'needs-setup': service = service_module.Service( - managed_services[0], title, ports=['http', 'https'], + managed_services[0], name, ports=['http', 'https'], is_external=True, is_enabled=is_enabled, enable=enable, disable=disable) if is_enabled(): add_shortcut() + domain_added.connect(on_domain_added) + domain_removed.connect(on_domain_removed) + domainname_change.connect(on_domainname_change) + def setup(helper, old_version=None): """Install and configure the module.""" - helper.call('pre', actions.superuser_run, 'cockpit', ['pre-setup']) helper.install(managed_packages) - helper.call('post', actions.superuser_run, 'cockpit', ['setup']) + domains = [domain + for domains_of_a_type in names.domains.values() + for domain in domains_of_a_type] + helper.call('post', actions.superuser_run, 'cockpit', ['setup'] + domains) global service if service is None: service = service_module.Service( - managed_services[0], title, ports=['http', 'https'], + managed_services[0], name, ports=['http', 'https'], is_external=True, is_enabled=is_enabled, enable=enable, disable=disable) helper.call('post', service.notify_enabled, None, True) @@ -87,25 +101,26 @@ def setup(helper, old_version=None): def add_shortcut(): - frontpage.add_shortcut('cockpit', title, url='/cockpit', + """Add a shortcut the frontpage.""" + frontpage.add_shortcut('cockpit', name, url='/_cockpit/', login_required=True) def is_enabled(): """Return whether the module is enabled.""" - return (action_utils.webserver_is_enabled('cockpit-plinth') and + return (action_utils.webserver_is_enabled('cockpit-freedombox') and action_utils.service_is_running('cockpit.socket')) def enable(): """Enable the module.""" - actions.superuser_run('cockpit.socket', ['enable']) + actions.superuser_run('cockpit', ['enable']) add_shortcut() def disable(): """Disable the module.""" - actions.superuser_run('cockpit.socket', ['disable']) + actions.superuser_run('cockpit', ['disable']) frontpage.remove_shortcut('cockpit') @@ -114,20 +129,23 @@ def diagnose(): results = [] results.extend(action_utils.diagnose_url_on_all( - 'https://{host}/cockpit', check_certificate=False)) + 'https://{host}/_cockpit/', check_certificate=False)) return results -def on_domain_added(): - actions.superuser_run('cockpit.socket', ['add_domain']) -def on_domain_removed(): - actions.superuser_run('cockpit.socket', ['remove_domain']) +def on_domain_added(sender, domain_type, name, description='', services=None, + **kwargs): + """Handle addition of a new domain.""" + actions.superuser_run('cockpit', ['add-domain', name]) + + +def on_domain_removed(sender, domain_type, name, **kwargs): + """Handle removal of a domain.""" + actions.superuser_run('cockpit', ['remove-domain', name]) + def on_domainname_change(sender, old_domainname, new_domainname, **kwargs): - del sender - del kwargs - actions.superuser_run('cockpit.socket', - ['change_domain', - '--domainname', new_domainname], - async=True) + """Handle change of a domain.""" + actions.superuser_run('cockpit', ['remove-domain', old_domainname]) + actions.superuser_run('cockpit', ['add-domain', new_domainname]) diff --git a/plinth/modules/cockpit/urls.py b/plinth/modules/cockpit/urls.py index 72a9e8096..34a2b5cc7 100644 --- a/plinth/modules/cockpit/urls.py +++ b/plinth/modules/cockpit/urls.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ URLs for Cockpit module. """ @@ -24,12 +23,12 @@ from django.conf.urls import url from plinth.views import ServiceView from plinth.modules import cockpit - urlpatterns = [ - url(r'^apps/cockpit/$', ServiceView.as_view( - service_id=cockpit.managed_services[0], - diagnostics_module_name="cockpit", - description=cockpit.description, - show_status_block=True - ), name='index'), + url(r'^sys/cockpit/$', + ServiceView.as_view( + service_id=cockpit.managed_services[0], + diagnostics_module_name='cockpit', + description=cockpit.description, + show_status_block=True), + name='index'), ]