Add config for password policy, audit and motd
This change basically takes a number of dashboard configuration options which are managed via `ceph dasboard key value` and exposes them as charm config options. Change-Id: I92e0948e36f4156686c908f86b5e2398b504a742
This commit is contained in:
parent
765c7cfd58
commit
0c279b8ad5
70
config.yaml
70
config.yaml
@ -2,9 +2,79 @@
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
options:
|
||||
debug:
|
||||
type: boolean
|
||||
default: False
|
||||
description: |
|
||||
Control debug mode. It is recommended that debug be disabled in
|
||||
production deployments.
|
||||
public-hostname:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
The hostname or address of the public endpoints created for the
|
||||
dashboard
|
||||
enable-password-policy:
|
||||
type: boolean
|
||||
default: True
|
||||
description: Enable password policy
|
||||
password-policy-check-length:
|
||||
type: boolean
|
||||
default: True
|
||||
description: |
|
||||
Reject password if it is shorter then password-policy-min-length
|
||||
password-policy-check-oldpwd:
|
||||
type: boolean
|
||||
default: True
|
||||
description: Reject password if it matches previous password.
|
||||
password-policy-check-username:
|
||||
type: boolean
|
||||
default: True
|
||||
description: Reject password if username is included in password.
|
||||
password-policy-check-exclusion-list:
|
||||
type: boolean
|
||||
default: True
|
||||
description: Reject password if it contains a word from a forbidden list.
|
||||
password-policy-check-complexity:
|
||||
type: boolean
|
||||
default: True
|
||||
description: |
|
||||
Check password meets a complexity score of password-policy-min-complexity.
|
||||
See https://docs.ceph.com/en/latest/mgr/dashboard/#password-policy
|
||||
password-policy-check-sequential-chars:
|
||||
type: boolean
|
||||
default: True
|
||||
description: |
|
||||
Reject password if it contains a sequence of sequential characters. e.g.
|
||||
a password containing '123' or 'efg' would be rejected.
|
||||
password-policy-check-repetitive-chars:
|
||||
type: boolean
|
||||
default: True
|
||||
description: |
|
||||
Reject password if password contains consecutive repeating charachters.
|
||||
password-policy-min-length:
|
||||
type: int
|
||||
default: 8
|
||||
description: Set minimum password length.
|
||||
password-policy-min-complexity:
|
||||
type: int
|
||||
default: 10
|
||||
description: |
|
||||
Set minimum password complexity score.
|
||||
See https://docs.ceph.com/en/latest/mgr/dashboard/#password-policy
|
||||
audit-api-enabled:
|
||||
type: boolean
|
||||
default: False
|
||||
description: |
|
||||
Log requests made to the dashboard REST API to the Ceph audit log.
|
||||
audit-api-log-payload:
|
||||
type: boolean
|
||||
default: True
|
||||
description: |
|
||||
Include payload in Ceph audit logs. audit-api-enabled must be set to True
|
||||
to enable this.,
|
||||
motd:
|
||||
type: string
|
||||
default: ""
|
||||
description: |
|
||||
Message of the day settings. Should be in the format "severity|expires|message". Set to "" to disable.
|
||||
|
123
src/charm.py
123
src/charm.py
@ -13,6 +13,8 @@ from ops.framework import StoredState
|
||||
from ops.main import main
|
||||
from ops.model import ActiveStatus, BlockedStatus, StatusBase
|
||||
from ops.charm import ActionEvent
|
||||
from typing import List, Union
|
||||
|
||||
import interface_tls_certificates.ca_client as ca_client
|
||||
import re
|
||||
import secrets
|
||||
@ -24,6 +26,7 @@ import interface_dashboard
|
||||
import interface_api_endpoints
|
||||
import cryptography.hazmat.primitives.serialization as serialization
|
||||
import charms_ceph.utils as ceph_utils
|
||||
import charmhelpers.core.host as ch_host
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@ -44,6 +47,98 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm):
|
||||
'/usr/local/share/ca-certificates/vault_ca_cert_dashboard.crt')
|
||||
TLS_PORT = 8443
|
||||
|
||||
class CharmCephOption():
|
||||
"""Manage a charm option to ceph command to manage that option"""
|
||||
|
||||
def __init__(self, charm_option_name, ceph_option_name,
|
||||
min_version=None):
|
||||
self.charm_option_name = charm_option_name
|
||||
self.ceph_option_name = ceph_option_name
|
||||
self.min_version = min_version
|
||||
|
||||
def is_supported(self) -> bool:
|
||||
"""Is the option supported on this unit"""
|
||||
if self.min_version:
|
||||
return self.minimum_supported(self.min_version)
|
||||
return True
|
||||
|
||||
def minimum_supported(self, supported_version: str) -> bool:
|
||||
"""Check if installed Ceph release is >= to supported_version"""
|
||||
return ch_host.cmp_pkgrevno('ceph-common', supported_version) < 1
|
||||
|
||||
def convert_option(self, value: Union[bool, str, int]) -> List[str]:
|
||||
"""Convert a value to the corresponding value part of the ceph
|
||||
dashboard command"""
|
||||
return [str(value)]
|
||||
|
||||
def ceph_command(self, value: List[str]) -> List[str]:
|
||||
"""Shell command to set option to desired value"""
|
||||
cmd = ['ceph', 'dashboard', self.ceph_option_name]
|
||||
cmd.extend(self.convert_option(value))
|
||||
return cmd
|
||||
|
||||
class DebugOption(CharmCephOption):
|
||||
|
||||
def convert_option(self, value):
|
||||
"""Convert charm True/False to enable/disable"""
|
||||
if value:
|
||||
return ['enable']
|
||||
else:
|
||||
return ['disable']
|
||||
|
||||
class MOTDOption(CharmCephOption):
|
||||
|
||||
def convert_option(self, value):
|
||||
"""Split motd charm option into ['severity', 'time', 'message']"""
|
||||
if value:
|
||||
return value.split('|')
|
||||
else:
|
||||
return ['clear']
|
||||
|
||||
CHARM_TO_CEPH_OPTIONS = [
|
||||
DebugOption('debug', 'debug'),
|
||||
CharmCephOption(
|
||||
'enable-password-policy',
|
||||
'set-pwd-policy-enabled'),
|
||||
CharmCephOption(
|
||||
'password-policy-check-length',
|
||||
'set-pwd-policy-check-length-enabled'),
|
||||
CharmCephOption(
|
||||
'password-policy-check-oldpwd',
|
||||
'set-pwd-policy-check-oldpwd-enabled'),
|
||||
CharmCephOption(
|
||||
'password-policy-check-username',
|
||||
'set-pwd-policy-check-username-enabled'),
|
||||
CharmCephOption(
|
||||
'password-policy-check-exclusion-list',
|
||||
'set-pwd-policy-check-exclusion-list-enabled'),
|
||||
CharmCephOption(
|
||||
'password-policy-check-complexity',
|
||||
'set-pwd-policy-check-complexity-enabled'),
|
||||
CharmCephOption(
|
||||
'password-policy-check-sequential-chars',
|
||||
'set-pwd-policy-check-sequential-chars-enabled'),
|
||||
CharmCephOption(
|
||||
'password-policy-check-repetitive-chars',
|
||||
'set-pwd-policy-check-repetitive-chars-enabled'),
|
||||
CharmCephOption(
|
||||
'password-policy-min-length',
|
||||
'set-pwd-policy-min-length'),
|
||||
CharmCephOption(
|
||||
'password-policy-min-complexity',
|
||||
'set-pwd-policy-min-complexity'),
|
||||
CharmCephOption(
|
||||
'audit-api-enabled',
|
||||
'set-audit-api-enabled'),
|
||||
CharmCephOption(
|
||||
'audit-api-log-payload',
|
||||
'set-audit-api-log-payload'),
|
||||
MOTDOption(
|
||||
'motd',
|
||||
'motd',
|
||||
min_version='15.2.14')
|
||||
]
|
||||
|
||||
def __init__(self, *args) -> None:
|
||||
"""Setup adapters and observers."""
|
||||
super().__init__(*args)
|
||||
@ -113,6 +208,33 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm):
|
||||
ceph_utils.mgr_disable_dashboard()
|
||||
ceph_utils.mgr_enable_dashboard()
|
||||
|
||||
def _run_cmd(self, cmd: List[str]) -> None:
|
||||
"""Run command in subprocess
|
||||
|
||||
`cmd` The command to run
|
||||
"""
|
||||
try:
|
||||
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
logging.exception("Command failed: {}".format(exc.output))
|
||||
|
||||
def _apply_ceph_config_from_charm_config(self) -> None:
|
||||
"""Read charm config and apply settings to dashboard config"""
|
||||
for option in self.CHARM_TO_CEPH_OPTIONS:
|
||||
try:
|
||||
value = self.config[option.charm_option_name]
|
||||
except KeyError:
|
||||
logging.error(
|
||||
"Unknown charm option {}, skipping".format(
|
||||
option.charm_option_name))
|
||||
continue
|
||||
if option.is_supported():
|
||||
self._run_cmd(option.ceph_command(value))
|
||||
else:
|
||||
logging.warning(
|
||||
"Skipping charm option {}, not supported".format(
|
||||
option.charm_option_name))
|
||||
|
||||
def _configure_dashboard(self, _) -> None:
|
||||
"""Configure dashboard"""
|
||||
if not self.mon.mons_ready:
|
||||
@ -120,6 +242,7 @@ class CephDashboardCharm(ops_openstack.core.OSBaseCharm):
|
||||
return
|
||||
if self.unit.is_leader() and not ceph_utils.is_dashboard_enabled():
|
||||
ceph_utils.mgr_enable_dashboard()
|
||||
self._apply_ceph_config_from_charm_config()
|
||||
ceph_utils.mgr_config_set(
|
||||
'mgr/dashboard/{hostname}/server_addr'.format(
|
||||
hostname=socket.gethostname()),
|
||||
|
@ -155,12 +155,19 @@ class TestCephDashboardCharmBase(CharmTestCase):
|
||||
PATCHES = [
|
||||
'ceph_utils',
|
||||
'socket',
|
||||
'subprocess'
|
||||
'subprocess',
|
||||
'ch_host',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp(charm, self.PATCHES)
|
||||
self.harness = Harness(
|
||||
self.harness = self.get_harness()
|
||||
|
||||
self.socket.gethostname.return_value = 'server1'
|
||||
self.socket.getfqdn.return_value = 'server1.local'
|
||||
|
||||
def get_harness(self):
|
||||
_harness = Harness(
|
||||
_CephDashboardCharm,
|
||||
)
|
||||
|
||||
@ -178,24 +185,78 @@ class TestCephDashboardCharmBase(CharmTestCase):
|
||||
'egress-subnets': ['10.0.0.0/24']}
|
||||
return network_data
|
||||
|
||||
self.harness._backend = _TestingOPSModelBackend(
|
||||
self.harness._unit_name, self.harness._meta)
|
||||
self.harness._model = model.Model(
|
||||
self.harness._meta,
|
||||
self.harness._backend)
|
||||
self.harness._framework = framework.Framework(
|
||||
_harness._backend = _TestingOPSModelBackend(
|
||||
_harness._unit_name, _harness._meta)
|
||||
_harness._model = model.Model(
|
||||
_harness._meta,
|
||||
_harness._backend)
|
||||
_harness._framework = framework.Framework(
|
||||
":memory:",
|
||||
self.harness._charm_dir,
|
||||
self.harness._meta,
|
||||
self.harness._model)
|
||||
_harness._charm_dir,
|
||||
_harness._meta,
|
||||
_harness._model)
|
||||
# END Workaround
|
||||
self.socket.gethostname.return_value = 'server1'
|
||||
self.socket.getfqdn.return_value = 'server1.local'
|
||||
return _harness
|
||||
|
||||
def test_init(self):
|
||||
self.harness.begin()
|
||||
self.assertFalse(self.harness.charm._stored.is_started)
|
||||
|
||||
def test_charm_config(self):
|
||||
self.ceph_utils.is_dashboard_enabled.return_value = True
|
||||
self.ch_host.cmp_pkgrevno.return_value = 0
|
||||
basic_boolean = [
|
||||
('enable-password-policy', 'set-pwd-policy-enabled'),
|
||||
('password-policy-check-length',
|
||||
'set-pwd-policy-check-length-enabled'),
|
||||
('password-policy-check-oldpwd',
|
||||
'set-pwd-policy-check-oldpwd-enabled'),
|
||||
('password-policy-check-username',
|
||||
'set-pwd-policy-check-username-enabled'),
|
||||
('password-policy-check-exclusion-list',
|
||||
'set-pwd-policy-check-exclusion-list-enabled'),
|
||||
('password-policy-check-complexity',
|
||||
'set-pwd-policy-check-complexity-enabled'),
|
||||
('password-policy-check-sequential-chars',
|
||||
'set-pwd-policy-check-sequential-chars-enabled'),
|
||||
('password-policy-check-repetitive-chars',
|
||||
'set-pwd-policy-check-repetitive-chars-enabled'),
|
||||
('audit-api-enabled',
|
||||
'set-audit-api-enabled'),
|
||||
('audit-api-log-payload',
|
||||
'set-audit-api-log-payload')]
|
||||
expect = []
|
||||
for charm_option, ceph_option in basic_boolean:
|
||||
expect.append((charm_option, True, [ceph_option, 'True']))
|
||||
expect.append((charm_option, False, [ceph_option, 'False']))
|
||||
expect.extend([
|
||||
('debug', True, ['debug', 'enable']),
|
||||
('debug', False, ['debug', 'disable'])])
|
||||
expect.extend([
|
||||
('motd', 'warning|5w|enough is enough', ['motd', 'warning', '5w',
|
||||
'enough is enough']),
|
||||
('motd', '', ['motd', 'clear'])])
|
||||
base_cmd = ['ceph', 'dashboard']
|
||||
for charm_option, charm_value, expected_options in expect:
|
||||
_harness = self.get_harness()
|
||||
rel_id = _harness.add_relation('dashboard', 'ceph-mon')
|
||||
_harness.add_relation_unit(
|
||||
rel_id,
|
||||
'ceph-mon/0')
|
||||
_harness.update_relation_data(
|
||||
rel_id,
|
||||
'ceph-mon/0',
|
||||
{
|
||||
'mon-ready': 'True'})
|
||||
_harness.begin()
|
||||
expected_cmd = base_cmd + expected_options
|
||||
self.subprocess.check_output.reset_mock()
|
||||
_harness.update_config(
|
||||
key_values={charm_option: charm_value})
|
||||
self.subprocess.check_output.assert_called_once_with(
|
||||
expected_cmd,
|
||||
stderr=self.subprocess.STDOUT)
|
||||
|
||||
def test__on_ca_available(self):
|
||||
rel_id = self.harness.add_relation('certificates', 'vault')
|
||||
self.harness.begin()
|
||||
|
Loading…
x
Reference in New Issue
Block a user