First rewrite of ceph-mon with operator framework

This patchset implements the first rewrite of the charm using the
operator framework by simply calling into the hooks.

This change also includes functional validation about charm upgrades
from the previous stable to the locally built charm.

Fix tempest breakage for python < 3.8

Co-authored-by: Chris MacNaughton <chris.macnaughton@canonical.com>

Change-Id: I61308bb2900134ea163d9e92444066a3cb0de43d
func-test-pr: https://github.com/openstack-charmers/zaza-openstack-tests/pull/849
This commit is contained in:
Luciano Lo Giudice
2022-05-11 17:39:53 -03:00
parent 32da1b0625
commit 1ee3d04fda
226 changed files with 195 additions and 32598 deletions

View File

@@ -1,34 +0,0 @@
#!/usr/bin/make
PYTHON := /usr/bin/env python3
lint:
@tox -e pep8
test:
@echo Starting unit tests...
@tox -e py27
functional_test:
@echo Starting Amulet tests...
@tox -e func27
bin/charm_helpers_sync.py:
@mkdir -p bin
@curl -o bin/charm_helpers_sync.py https://raw.githubusercontent.com/juju/charm-helpers/master/tools/charm_helpers_sync/charm_helpers_sync.py
bin/git_sync.py:
@mkdir -p bin
@wget -O bin/git_sync.py https://raw.githubusercontent.com/CanonicalLtd/git-sync/master/git_sync.py
ch-sync: bin/charm_helpers_sync.py
$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
ceph-sync: bin/git_sync.py
$(PYTHON) bin/git_sync.py -d lib -s https://github.com/openstack/charms.ceph.git
sync: ch-sync
publish: lint test
bzr push lp:charms/ceph-mon
bzr push lp:charms/trusty/ceph-mon

6
TODO
View File

@@ -1,6 +0,0 @@
Ceph Charm
==========
* fix tunables (http://tracker.newdream.net/issues/2210)
* more than 192 PGs
* fixup data placement in crush to be host not osd driven

View File

@@ -11,6 +11,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')

View File

@@ -14,9 +14,6 @@
import json import json
from subprocess import CalledProcessError, check_output from subprocess import CalledProcessError, check_output
import sys
sys.path.append('hooks')
from charmhelpers.core.hookenv import ( from charmhelpers.core.hookenv import (
action_get, action_get,

View File

@@ -16,11 +16,6 @@
"""Changes the crush weight of an OSD.""" """Changes the crush weight of an OSD."""
import sys
sys.path.append("lib")
sys.path.append("hooks")
from charmhelpers.core.hookenv import function_fail, function_get, log from charmhelpers.core.hookenv import function_fail, function_get, log
from charms_ceph.utils import reweight_osd from charms_ceph.utils import reweight_osd

View File

@@ -14,11 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
import subprocess import subprocess
sys.path.append('hooks')
import charmhelpers.core.hookenv as hookenv import charmhelpers.core.hookenv as hookenv

View File

@@ -15,9 +15,6 @@
# limitations under the License. # limitations under the License.
from subprocess import CalledProcessError from subprocess import CalledProcessError
import sys
sys.path.append('hooks')
from charmhelpers.contrib.storage.linux.ceph import Pool, pool_exists from charmhelpers.contrib.storage.linux.ceph import Pool, pool_exists
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail

View File

@@ -14,11 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
import subprocess import subprocess
sys.path.append('hooks')
import charmhelpers.core.hookenv as hookenv import charmhelpers.core.hookenv as hookenv

View File

@@ -15,9 +15,6 @@
# limitations under the License. # limitations under the License.
from subprocess import CalledProcessError from subprocess import CalledProcessError
import sys
sys.path.append('hooks')
from charmhelpers.contrib.storage.linux.ceph import create_erasure_profile from charmhelpers.contrib.storage.linux.ceph import create_erasure_profile
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail

View File

@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from subprocess import CalledProcessError from subprocess import CalledProcessError
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail
from charmhelpers.contrib.storage.linux.ceph import ErasurePool, ReplicatedPool from charmhelpers.contrib.storage.linux.ceph import ErasurePool, ReplicatedPool

View File

@@ -13,6 +13,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import base64 import base64
from charmhelpers.core.hookenv import action_get, action_fail from charmhelpers.core.hookenv import action_get, action_fail
from subprocess import check_output, CalledProcessError, PIPE, Popen from subprocess import check_output, CalledProcessError, PIPE, Popen

View File

@@ -17,9 +17,6 @@
from subprocess import CalledProcessError from subprocess import CalledProcessError
__author__ = 'chris' __author__ = 'chris'
import sys
sys.path.append('hooks')
from charmhelpers.contrib.storage.linux.ceph import remove_erasure_profile from charmhelpers.contrib.storage.linux.ceph import remove_erasure_profile
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail

View File

@@ -14,11 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
import subprocess import subprocess
sys.path.append('hooks')
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail

View File

@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from charmhelpers.core.hookenv import action_get, action_fail, action_set, log from charmhelpers.core.hookenv import action_get, action_fail, action_set, log
from subprocess import CalledProcessError, check_output, STDOUT from subprocess import CalledProcessError, check_output, STDOUT

View File

@@ -14,10 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from charmhelpers.contrib.storage.linux.ceph import get_erasure_profile from charmhelpers.contrib.storage.linux.ceph import get_erasure_profile
from charmhelpers.core.hookenv import action_get, action_set from charmhelpers.core.hookenv import action_get, action_set

View File

@@ -14,10 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
import json import json
sys.path.append("hooks")
from charmhelpers.core.hookenv import action_get, action_fail, action_set, log from charmhelpers.core.hookenv import action_get, action_fail, action_set, log
from subprocess import CalledProcessError, check_output from subprocess import CalledProcessError, check_output

View File

@@ -16,12 +16,9 @@
"""Run action to collect Ceph quorum_status output.""" """Run action to collect Ceph quorum_status output."""
import json import json
import sys
from subprocess import CalledProcessError from subprocess import CalledProcessError
sys.path.append('hooks')
from ceph_ops import get_quorum_status from ceph_ops import get_quorum_status
from charmhelpers.core.hookenv import function_fail, function_get, function_set from charmhelpers.core.hookenv import function_fail, function_get, function_set

View File

@@ -13,24 +13,11 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json import json
import os
import sys
import yaml import yaml
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
_path = os.path.dirname(os.path.realpath(__file__))
_hooks = os.path.abspath(os.path.join(_path, "../hooks"))
def _add_path(path):
if path not in sys.path:
sys.path.insert(1, path)
_add_path(_hooks)
from charmhelpers.core.hookenv import ( from charmhelpers.core.hookenv import (
ERROR, ERROR,
log, log,

View File

@@ -14,11 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
sys.path.append('hooks')
from charmhelpers.core.hookenv import action_get, log, action_set, action_fail from charmhelpers.core.hookenv import action_get, log, action_set, action_fail
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -16,12 +16,9 @@
import json import json
import re import re
import sys
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
import yaml import yaml
sys.path.append('hooks')
from charmhelpers.core.hookenv import function_fail, function_get, \ from charmhelpers.core.hookenv import function_fail, function_get, \
function_set, log function_set, log

View File

@@ -13,23 +13,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json import json
import os
import sys
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
_path = os.path.dirname(os.path.realpath(__file__))
_hooks = os.path.abspath(os.path.join(_path, "../hooks"))
def _add_path(path):
if path not in sys.path:
sys.path.insert(1, path)
_add_path(_hooks)
from charmhelpers.core.hookenv import ( from charmhelpers.core.hookenv import (
log, log,
function_fail, function_fail,

View File

@@ -13,24 +13,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json import json
import os
import sys
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
_path = os.path.dirname(os.path.realpath(__file__))
_hooks = os.path.abspath(os.path.join(_path, "../hooks"))
_lib = os.path.abspath(os.path.join(_path, "../lib"))
def _add_path(path):
if path not in sys.path:
sys.path.insert(1, path)
_add_path(_hooks)
_add_path(_lib)
from charmhelpers.core.hookenv import ( from charmhelpers.core.hookenv import (
log, log,

View File

@@ -14,11 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
sys.path.append('hooks')
from charmhelpers.core.hookenv import log, action_set, action_get, action_fail from charmhelpers.core.hookenv import log, action_set, action_get, action_fail
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -15,10 +15,6 @@
# limitations under the License. # limitations under the License.
from subprocess import CalledProcessError from subprocess import CalledProcessError
import sys
sys.path.append('lib')
sys.path.append('hooks')
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail
from charms_ceph.broker import handle_set_pool_value from charms_ceph.broker import handle_set_pool_value

View File

@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
from charmhelpers.core.hookenv import log, action_set, action_fail from charmhelpers.core.hookenv import log, action_set, action_fail

View File

@@ -26,16 +26,12 @@ from subprocess import (
CalledProcessError, CalledProcessError,
) )
import sys
sys.path.append('lib')
sys.path.append('hooks')
from charmhelpers.core.hookenv import ( from charmhelpers.core.hookenv import (
function_get, function_get,
log, log,
function_fail function_fail
) )
from charmhelpers.core.host import cmp_pkgrevno from charmhelpers.core.host import cmp_pkgrevno
from charmhelpers.contrib.storage.linux import ceph from charmhelpers.contrib.storage.linux import ceph
from charms_ceph.utils import get_osd_weight from charms_ceph.utils import get_osd_weight

View File

@@ -15,9 +15,6 @@
# limitations under the License. # limitations under the License.
from subprocess import CalledProcessError from subprocess import CalledProcessError
import sys
sys.path.append('hooks')
from charmhelpers.contrib.storage.linux.ceph import Pool, pool_exists from charmhelpers.contrib.storage.linux.ceph import Pool, pool_exists
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail

View File

@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from subprocess import CalledProcessError from subprocess import CalledProcessError
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail
from charmhelpers.contrib.storage.linux.ceph import remove_pool_snapshot from charmhelpers.contrib.storage.linux.ceph import remove_pool_snapshot

View File

@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from subprocess import CalledProcessError from subprocess import CalledProcessError
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail
from charmhelpers.contrib.storage.linux.ceph import rename_pool from charmhelpers.contrib.storage.linux.ceph import rename_pool

View File

@@ -16,8 +16,6 @@
import sys import sys
sys.path.append('hooks')
import charmhelpers.contrib.openstack.audits as audits import charmhelpers.contrib.openstack.audits as audits
from charmhelpers.contrib.openstack.audits import ( from charmhelpers.contrib.openstack.audits import (
openstack_security_guide, openstack_security_guide,

View File

@@ -14,10 +14,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from charmhelpers.core.hookenv import action_set, action_fail from charmhelpers.core.hookenv import action_set, action_fail
sys.path.append('lib')
from charms_ceph.utils import osd_noout from charms_ceph.utils import osd_noout
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from subprocess import CalledProcessError from subprocess import CalledProcessError
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail
from charmhelpers.contrib.storage.linux.ceph import set_pool_quota from charmhelpers.contrib.storage.linux.ceph import set_pool_quota

View File

@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
from charmhelpers.core.hookenv import log, action_get, action_set, action_fail from charmhelpers.core.hookenv import log, action_get, action_set, action_fail

View File

@@ -14,9 +14,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from subprocess import CalledProcessError from subprocess import CalledProcessError
from charmhelpers.core.hookenv import action_get, log, action_fail from charmhelpers.core.hookenv import action_get, log, action_fail
from charmhelpers.contrib.storage.linux.ceph import snapshot_pool from charmhelpers.contrib.storage.linux.ceph import snapshot_pool

View File

@@ -14,10 +14,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
sys.path.append('hooks')
from charmhelpers.core.hookenv import action_set, action_fail from charmhelpers.core.hookenv import action_set, action_fail
sys.path.append('lib')
from charms_ceph.utils import osd_noout from charms_ceph.utils import osd_noout
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,21 +0,0 @@
repo: https://github.com/juju/charm-helpers
destination: hooks/charmhelpers
include:
- core
- osplatform
- cli
- fetch
- contrib.storage.linux
- payload.execd
- contrib.openstack
- contrib.network.ip
- contrib.hahelpers
- contrib.openstack:
- alternatives
- audits
- exceptions
- utils
- contrib.charmsupport
- contrib.hardening|inc=*
- fetch.python
- contrib.openstack.policyd

View File

@@ -2,23 +2,25 @@ type: charm
parts: parts:
charm: charm:
plugin: dump
source: .
prime: prime:
- actions/* - actions/*
- files/*
- hooks/*
- lib/* - lib/*
- templates/* - templates/*
- actions.yaml after:
- config.yaml - update-certificates
- copyright charm-python-packages:
- hardening.yaml # Use the updated version of setuptools (needed by jinja2).
- icon.svg - setuptools
- LICENSE build-packages:
- Makefile - git
- metadata.yaml
- README.md update-certificates:
# Ensure that certificates in the base image are up-to-date.
plugin: nil
override-build: |
apt update
apt install -y ca-certificates
update-ca-certificates
bases: bases:
- build-on: - build-on:

View File

@@ -1,13 +0,0 @@
# Copyright 2016 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@@ -1 +0,0 @@
ceph_hooks.py

View File

@@ -1 +0,0 @@
ceph_hooks.py

View File

@@ -1 +0,0 @@
ceph_hooks.py

View File

@@ -1 +0,0 @@
ceph_hooks.py

View File

@@ -1,84 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Bootstrap charm-helpers, installing its dependencies if necessary using
# only standard libraries.
import functools
import inspect
import subprocess
try:
import yaml # NOQA:F401
except ImportError:
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
import yaml # NOQA:F401
# Holds a list of mapping of mangled function names that have been deprecated
# using the @deprecate decorator below. This is so that the warning is only
# printed once for each usage of the function.
__deprecated_functions = {}
def deprecate(warning, date=None, log=None):
"""Add a deprecation warning the first time the function is used.
The date which is a string in semi-ISO8660 format indicates the year-month
that the function is officially going to be removed.
usage:
@deprecate('use core/fetch/add_source() instead', '2017-04')
def contributed_add_source_thing(...):
...
And it then prints to the log ONCE that the function is deprecated.
The reason for passing the logging function (log) is so that hookenv.log
can be used for a charm if needed.
:param warning: String to indicate what is to be used instead.
:param date: Optional string in YYYY-MM format to indicate when the
function will definitely (probably) be removed.
:param log: The log function to call in order to log. If None, logs to
stdout
"""
def wrap(f):
@functools.wraps(f)
def wrapped_f(*args, **kwargs):
try:
module = inspect.getmodule(f)
file = inspect.getsourcefile(f)
lines = inspect.getsourcelines(f)
f_name = "{}-{}-{}..{}-{}".format(
module.__name__, file, lines[0], lines[-1], f.__name__)
except (IOError, TypeError):
# assume it was local, so just use the name of the function
f_name = f.__name__
if f_name not in __deprecated_functions:
__deprecated_functions[f_name] = True
s = "DEPRECATION WARNING: Function {} is being removed".format(
f.__name__)
if date:
s = "{} on/around {}".format(s, date)
if warning:
s = "{} : {}".format(s, warning)
if log:
log(s)
else:
print(s)
return f(*args, **kwargs)
return wrapped_f
return wrap

View File

@@ -1,187 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import argparse
import sys
import charmhelpers.core.unitdata
class OutputFormatter(object):
def __init__(self, outfile=sys.stdout):
self.formats = (
"raw",
"json",
"py",
"yaml",
"csv",
"tab",
)
self.outfile = outfile
def add_arguments(self, argument_parser):
formatgroup = argument_parser.add_mutually_exclusive_group()
choices = self.supported_formats
formatgroup.add_argument("--format", metavar='FMT',
help="Select output format for returned data, "
"where FMT is one of: {}".format(choices),
choices=choices, default='raw')
for fmt in self.formats:
fmtfunc = getattr(self, fmt)
formatgroup.add_argument("-{}".format(fmt[0]),
"--{}".format(fmt), action='store_const',
const=fmt, dest='format',
help=fmtfunc.__doc__)
@property
def supported_formats(self):
return self.formats
def raw(self, output):
"""Output data as raw string (default)"""
if isinstance(output, (list, tuple)):
output = '\n'.join(map(str, output))
self.outfile.write(str(output))
def py(self, output):
"""Output data as a nicely-formatted python data structure"""
import pprint
pprint.pprint(output, stream=self.outfile)
def json(self, output):
"""Output data in JSON format"""
import json
json.dump(output, self.outfile)
def yaml(self, output):
"""Output data in YAML format"""
import yaml
yaml.safe_dump(output, self.outfile)
def csv(self, output):
"""Output data as excel-compatible CSV"""
import csv
csvwriter = csv.writer(self.outfile)
csvwriter.writerows(output)
def tab(self, output):
"""Output data in excel-compatible tab-delimited format"""
import csv
csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
csvwriter.writerows(output)
def format_output(self, output, fmt='raw'):
fmtfunc = getattr(self, fmt)
fmtfunc(output)
class CommandLine(object):
argument_parser = None
subparsers = None
formatter = None
exit_code = 0
def __init__(self):
if not self.argument_parser:
self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
if not self.formatter:
self.formatter = OutputFormatter()
self.formatter.add_arguments(self.argument_parser)
if not self.subparsers:
self.subparsers = self.argument_parser.add_subparsers(help='Commands')
def subcommand(self, command_name=None):
"""
Decorate a function as a subcommand. Use its arguments as the
command-line arguments"""
def wrapper(decorated):
cmd_name = command_name or decorated.__name__
subparser = self.subparsers.add_parser(cmd_name,
description=decorated.__doc__)
for args, kwargs in describe_arguments(decorated):
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=decorated)
return decorated
return wrapper
def test_command(self, decorated):
"""
Subcommand is a boolean test function, so bool return values should be
converted to a 0/1 exit code.
"""
decorated._cli_test_command = True
return decorated
def no_output(self, decorated):
"""
Subcommand is not expected to return a value, so don't print a spurious None.
"""
decorated._cli_no_output = True
return decorated
def subcommand_builder(self, command_name, description=None):
"""
Decorate a function that builds a subcommand. Builders should accept a
single argument (the subparser instance) and return the function to be
run as the command."""
def wrapper(decorated):
subparser = self.subparsers.add_parser(command_name)
func = decorated(subparser)
subparser.set_defaults(func=func)
subparser.description = description or func.__doc__
return wrapper
def run(self):
"Run cli, processing arguments and executing subcommands."
arguments = self.argument_parser.parse_args()
argspec = inspect.getfullargspec(arguments.func)
vargs = []
for arg in argspec.args:
vargs.append(getattr(arguments, arg))
if argspec.varargs:
vargs.extend(getattr(arguments, argspec.varargs))
output = arguments.func(*vargs)
if getattr(arguments.func, '_cli_test_command', False):
self.exit_code = 0 if output else 1
output = ''
if getattr(arguments.func, '_cli_no_output', False):
output = ''
self.formatter.format_output(output, arguments.format)
if charmhelpers.core.unitdata._KV:
charmhelpers.core.unitdata._KV.flush()
cmdline = CommandLine()
def describe_arguments(func):
"""
Analyze a function's signature and return a data structure suitable for
passing in as arguments to an argparse parser's add_argument() method."""
argspec = inspect.getfullargspec(func)
# we should probably raise an exception somewhere if func includes **kwargs
if argspec.defaults:
positional_args = argspec.args[:-len(argspec.defaults)]
keyword_names = argspec.args[-len(argspec.defaults):]
for arg, default in zip(keyword_names, argspec.defaults):
yield ('--{}'.format(arg),), {'default': default}
else:
positional_args = argspec.args
for arg in positional_args:
yield (arg,), {}
if argspec.varargs:
yield (argspec.varargs,), {'nargs': '*'}

View File

@@ -1,34 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import cmdline
from charmhelpers.contrib.benchmark import Benchmark
@cmdline.subcommand(command_name='benchmark-start')
def start():
Benchmark.start()
@cmdline.subcommand(command_name='benchmark-finish')
def finish():
Benchmark.finish()
@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
def service(subparser):
subparser.add_argument("value", help="The composite score.")
subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
return Benchmark.set_composite_score

View File

@@ -1,30 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module loads sub-modules into the python runtime so they can be
discovered via the inspect module. In order to prevent flake8 from (rightfully)
telling us these are unused modules, throw a ' # noqa' at the end of each import
so that the warning is suppressed.
"""
from . import CommandLine # noqa
"""
Import the sub-modules which have decorated subcommands to register with chlp.
"""
from . import host # noqa
from . import benchmark # noqa
from . import unitdata # noqa
from . import hookenv # noqa

View File

@@ -1,21 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import cmdline
from charmhelpers.core import hookenv
cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
cmdline.subcommand('service-name')(hookenv.service_name)
cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)

View File

@@ -1,29 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import cmdline
from charmhelpers.core import host
@cmdline.subcommand()
def mounts():
"List mounts"
return host.mounts()
@cmdline.subcommand_builder('service', description="Control system services")
def service(subparser):
subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
subparser.add_argument("service_name", help="Name of the service to control")
return host.service

View File

@@ -1,46 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import cmdline
from charmhelpers.core import unitdata
@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
def unitdata_cmd(subparser):
nested = subparser.add_subparsers()
get_cmd = nested.add_parser('get', help='Retrieve data')
get_cmd.add_argument('key', help='Key to retrieve the value of')
get_cmd.set_defaults(action='get', value=None)
getrange_cmd = nested.add_parser('getrange', help='Retrieve multiple data')
getrange_cmd.add_argument('key', metavar='prefix',
help='Prefix of the keys to retrieve')
getrange_cmd.set_defaults(action='getrange', value=None)
set_cmd = nested.add_parser('set', help='Store data')
set_cmd.add_argument('key', help='Key to set')
set_cmd.add_argument('value', help='Value to store')
set_cmd.set_defaults(action='set')
def _unitdata_cmd(action, key, value):
if action == 'get':
return unitdata.kv().get(key)
elif action == 'getrange':
return unitdata.kv().getrange(key)
elif action == 'set':
unitdata.kv().set(key, value)
unitdata.kv().flush()
return ''
return _unitdata_cmd

View File

@@ -1,13 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@@ -1,13 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@@ -1,560 +0,0 @@
# Copyright 2012-2021 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Compatibility with the nrpe-external-master charm"""
#
# Authors:
# Matthew Wedgwood <matthew.wedgwood@canonical.com>
import glob
import grp
import os
import pwd
import re
import shlex
import shutil
import subprocess
import yaml
from charmhelpers.core.hookenv import (
application_name,
config,
hook_name,
local_unit,
log,
relation_get,
relation_ids,
relation_set,
relations_of_type,
)
from charmhelpers.core.host import service
from charmhelpers.core import host
# This module adds compatibility with the nrpe-external-master and plain nrpe
# subordinate charms. To use it in your charm:
#
# 1. Update metadata.yaml
#
# provides:
# (...)
# nrpe-external-master:
# interface: nrpe-external-master
# scope: container
#
# and/or
#
# provides:
# (...)
# local-monitors:
# interface: local-monitors
# scope: container
#
# 2. Add the following to config.yaml
#
# nagios_context:
# default: "juju"
# type: string
# description: |
# Used by the nrpe subordinate charms.
# A string that will be prepended to instance name to set the host name
# in nagios. So for instance the hostname would be something like:
# juju-myservice-0
# If you're running multiple environments with the same services in them
# this allows you to differentiate between them.
# nagios_servicegroups:
# default: ""
# type: string
# description: |
# A comma-separated list of nagios servicegroups.
# If left empty, the nagios_context will be used as the servicegroup
#
# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
#
# 4. Update your hooks.py with something like this:
#
# from charmsupport.nrpe import NRPE
# (...)
# def update_nrpe_config():
# nrpe_compat = NRPE()
# nrpe_compat.add_check(
# shortname = "myservice",
# description = "Check MyService",
# check_cmd = "check_http -w 2 -c 10 http://localhost"
# )
# nrpe_compat.add_check(
# "myservice_other",
# "Check for widget failures",
# check_cmd = "/srv/myapp/scripts/widget_check"
# )
# nrpe_compat.write()
#
# def config_changed():
# (...)
# update_nrpe_config()
#
# def nrpe_external_master_relation_changed():
# update_nrpe_config()
#
# def local_monitors_relation_changed():
# update_nrpe_config()
#
# 4.a If your charm is a subordinate charm set primary=False
#
# from charmsupport.nrpe import NRPE
# (...)
# def update_nrpe_config():
# nrpe_compat = NRPE(primary=False)
#
# 5. ln -s hooks.py nrpe-external-master-relation-changed
# ln -s hooks.py local-monitors-relation-changed
class CheckException(Exception):
pass
class Check(object):
shortname_re = '[A-Za-z0-9-_.@]+$'
service_template = ("""
#---------------------------------------------------
# This file is Juju managed
#---------------------------------------------------
define service {{
use active-service
host_name {nagios_hostname}
service_description {nagios_hostname}[{shortname}] """
"""{description}
check_command check_nrpe!{command}
servicegroups {nagios_servicegroup}
{service_config_overrides}
}}
""")
def __init__(self, shortname, description, check_cmd, max_check_attempts=None):
super(Check, self).__init__()
# XXX: could be better to calculate this from the service name
if not re.match(self.shortname_re, shortname):
raise CheckException("shortname must match {}".format(
Check.shortname_re))
self.shortname = shortname
self.command = "check_{}".format(shortname)
# Note: a set of invalid characters is defined by the
# Nagios server config
# The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
self.description = description
self.check_cmd = self._locate_cmd(check_cmd)
self.max_check_attempts = max_check_attempts
def _get_check_filename(self):
return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
def _get_service_filename(self, hostname):
return os.path.join(NRPE.nagios_exportdir,
'service__{}_{}.cfg'.format(hostname, self.command))
def _locate_cmd(self, check_cmd):
search_path = (
'/usr/lib/nagios/plugins',
'/usr/local/lib/nagios/plugins',
)
parts = shlex.split(check_cmd)
for path in search_path:
if os.path.exists(os.path.join(path, parts[0])):
command = os.path.join(path, parts[0])
if len(parts) > 1:
safe_args = [shlex.quote(arg) for arg in parts[1:]]
command += " " + " ".join(safe_args)
return command
log('Check command not found: {}'.format(parts[0]))
return ''
def _remove_service_files(self):
if not os.path.exists(NRPE.nagios_exportdir):
return
for f in os.listdir(NRPE.nagios_exportdir):
if f.endswith('_{}.cfg'.format(self.command)):
os.remove(os.path.join(NRPE.nagios_exportdir, f))
def remove(self, hostname):
nrpe_check_file = self._get_check_filename()
if os.path.exists(nrpe_check_file):
os.remove(nrpe_check_file)
self._remove_service_files()
def write(self, nagios_context, hostname, nagios_servicegroups):
nrpe_check_file = self._get_check_filename()
with open(nrpe_check_file, 'w') as nrpe_check_config:
nrpe_check_config.write("# check {}\n".format(self.shortname))
if nagios_servicegroups:
nrpe_check_config.write(
"# The following header was added automatically by juju\n")
nrpe_check_config.write(
"# Modifying it will affect nagios monitoring and alerting\n")
nrpe_check_config.write(
"# servicegroups: {}\n".format(nagios_servicegroups))
nrpe_check_config.write("command[{}]={}\n".format(
self.command, self.check_cmd))
if not os.path.exists(NRPE.nagios_exportdir):
log('Not writing service config as {} is not accessible'.format(
NRPE.nagios_exportdir))
else:
self.write_service_config(nagios_context, hostname,
nagios_servicegroups)
def write_service_config(self, nagios_context, hostname,
nagios_servicegroups):
self._remove_service_files()
if self.max_check_attempts:
service_config_overrides = ' max_check_attempts {}'.format(
self.max_check_attempts
) # Note indentation is here rather than in the template to avoid trailing spaces
else:
service_config_overrides = '' # empty string to avoid printing 'None'
templ_vars = {
'nagios_hostname': hostname,
'nagios_servicegroup': nagios_servicegroups,
'description': self.description,
'shortname': self.shortname,
'command': self.command,
'service_config_overrides': service_config_overrides,
}
nrpe_service_text = Check.service_template.format(**templ_vars)
nrpe_service_file = self._get_service_filename(hostname)
with open(nrpe_service_file, 'w') as nrpe_service_config:
nrpe_service_config.write(str(nrpe_service_text))
def run(self):
subprocess.call(self.check_cmd)
class NRPE(object):
nagios_logdir = '/var/log/nagios'
nagios_exportdir = '/var/lib/nagios/export'
nrpe_confdir = '/etc/nagios/nrpe.d'
homedir = '/var/lib/nagios' # home dir provided by nagios-nrpe-server
def __init__(self, hostname=None, primary=True):
super(NRPE, self).__init__()
self.config = config()
self.primary = primary
self.nagios_context = self.config['nagios_context']
if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
self.nagios_servicegroups = self.config['nagios_servicegroups']
else:
self.nagios_servicegroups = self.nagios_context
self.unit_name = local_unit().replace('/', '-')
if hostname:
self.hostname = hostname
else:
nagios_hostname = get_nagios_hostname()
if nagios_hostname:
self.hostname = nagios_hostname
else:
self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
self.checks = []
# Iff in an nrpe-external-master relation hook, set primary status
relation = relation_ids('nrpe-external-master')
if relation:
log("Setting charm primary status {}".format(primary))
for rid in relation:
relation_set(relation_id=rid, relation_settings={'primary': self.primary})
self.remove_check_queue = set()
@classmethod
def does_nrpe_conf_dir_exist(cls):
"""Return True if th nrpe_confdif directory exists."""
return os.path.isdir(cls.nrpe_confdir)
def add_check(self, *args, **kwargs):
shortname = None
if kwargs.get('shortname') is None:
if len(args) > 0:
shortname = args[0]
else:
shortname = kwargs['shortname']
self.checks.append(Check(*args, **kwargs))
try:
self.remove_check_queue.remove(shortname)
except KeyError:
pass
def remove_check(self, *args, **kwargs):
if kwargs.get('shortname') is None:
raise ValueError('shortname of check must be specified')
# Use sensible defaults if they're not specified - these are not
# actually used during removal, but they're required for constructing
# the Check object; check_disk is chosen because it's part of the
# nagios-plugins-basic package.
if kwargs.get('check_cmd') is None:
kwargs['check_cmd'] = 'check_disk'
if kwargs.get('description') is None:
kwargs['description'] = ''
check = Check(*args, **kwargs)
check.remove(self.hostname)
self.remove_check_queue.add(kwargs['shortname'])
def write(self):
try:
nagios_uid = pwd.getpwnam('nagios').pw_uid
nagios_gid = grp.getgrnam('nagios').gr_gid
except Exception:
log("Nagios user not set up, nrpe checks not updated")
return
if not os.path.exists(NRPE.nagios_logdir):
os.mkdir(NRPE.nagios_logdir)
os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
nrpe_monitors = {}
monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
# check that the charm can write to the conf dir. If not, then nagios
# probably isn't installed, and we can defer.
if not self.does_nrpe_conf_dir_exist():
return
for nrpecheck in self.checks:
nrpecheck.write(self.nagios_context, self.hostname,
self.nagios_servicegroups)
nrpe_monitors[nrpecheck.shortname] = {
"command": nrpecheck.command,
}
# If we were passed max_check_attempts, add that to the relation data
if nrpecheck.max_check_attempts is not None:
nrpe_monitors[nrpecheck.shortname]['max_check_attempts'] = nrpecheck.max_check_attempts
# update-status hooks are configured to firing every 5 minutes by
# default. When nagios-nrpe-server is restarted, the nagios server
# reports checks failing causing unnecessary alerts. Let's not restart
# on update-status hooks.
if not hook_name() == 'update-status':
service('restart', 'nagios-nrpe-server')
monitor_ids = relation_ids("local-monitors") + \
relation_ids("nrpe-external-master")
for rid in monitor_ids:
reldata = relation_get(unit=local_unit(), rid=rid)
if 'monitors' in reldata:
# update the existing set of monitors with the new data
old_monitors = yaml.safe_load(reldata['monitors'])
old_nrpe_monitors = old_monitors['monitors']['remote']['nrpe']
# remove keys that are in the remove_check_queue
old_nrpe_monitors = {k: v for k, v in old_nrpe_monitors.items()
if k not in self.remove_check_queue}
# update/add nrpe_monitors
old_nrpe_monitors.update(nrpe_monitors)
old_monitors['monitors']['remote']['nrpe'] = old_nrpe_monitors
# write back to the relation
relation_set(relation_id=rid, monitors=yaml.dump(old_monitors))
else:
# write a brand new set of monitors, as no existing ones.
relation_set(relation_id=rid, monitors=yaml.dump(monitors))
self.remove_check_queue.clear()
def get_nagios_hostcontext(relation_name='nrpe-external-master'):
"""
Query relation with nrpe subordinate, return the nagios_host_context
:param str relation_name: Name of relation nrpe sub joined to
"""
for rel in relations_of_type(relation_name):
if 'nagios_host_context' in rel:
return rel['nagios_host_context']
def get_nagios_hostname(relation_name='nrpe-external-master'):
"""
Query relation with nrpe subordinate, return the nagios_hostname
:param str relation_name: Name of relation nrpe sub joined to
"""
for rel in relations_of_type(relation_name):
if 'nagios_hostname' in rel:
return rel['nagios_hostname']
def get_nagios_unit_name(relation_name='nrpe-external-master'):
"""
Return the nagios unit name prepended with host_context if needed
:param str relation_name: Name of relation nrpe sub joined to
"""
host_context = get_nagios_hostcontext(relation_name)
if host_context:
unit = "%s:%s" % (host_context, local_unit())
else:
unit = local_unit()
return unit
def add_init_service_checks(nrpe, services, unit_name, immediate_check=True):
"""
Add checks for each service in list
:param NRPE nrpe: NRPE object to add check to
:param list services: List of services to check
:param str unit_name: Unit name to use in check description
:param bool immediate_check: For sysv init, run the service check immediately
"""
for svc in services:
# Don't add a check for these services from neutron-gateway
if svc in ['ext-port', 'os-charm-phy-nic-mtu']:
next
upstart_init = '/etc/init/%s.conf' % svc
sysv_init = '/etc/init.d/%s' % svc
if host.init_is_systemd(service_name=svc):
nrpe.add_check(
shortname=svc,
description='process check {%s}' % unit_name,
check_cmd='check_systemd.py %s' % svc
)
elif os.path.exists(upstart_init):
nrpe.add_check(
shortname=svc,
description='process check {%s}' % unit_name,
check_cmd='check_upstart_job %s' % svc
)
elif os.path.exists(sysv_init):
cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
checkpath = '%s/service-check-%s.txt' % (nrpe.homedir, svc)
croncmd = (
'/usr/local/lib/nagios/plugins/check_exit_status.pl '
'-e -s /etc/init.d/%s status' % svc
)
cron_file = '*/5 * * * * root %s > %s\n' % (croncmd, checkpath)
f = open(cronpath, 'w')
f.write(cron_file)
f.close()
nrpe.add_check(
shortname=svc,
description='service check {%s}' % unit_name,
check_cmd='check_status_file.py -f %s' % checkpath,
)
# if /var/lib/nagios doesn't exist open(checkpath, 'w') will fail
# (LP: #1670223).
if immediate_check and os.path.isdir(nrpe.homedir):
f = open(checkpath, 'w')
subprocess.call(
croncmd.split(),
stdout=f,
stderr=subprocess.STDOUT
)
f.close()
os.chmod(checkpath, 0o644)
def copy_nrpe_checks(nrpe_files_dir=None):
"""
Copy the nrpe checks into place
"""
NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
if nrpe_files_dir is None:
# determine if "charmhelpers" is in CHARMDIR or CHARMDIR/hooks
for segment in ['.', 'hooks']:
nrpe_files_dir = os.path.abspath(os.path.join(
os.getenv('CHARM_DIR'),
segment,
'charmhelpers',
'contrib',
'openstack',
'files'))
if os.path.isdir(nrpe_files_dir):
break
else:
raise RuntimeError("Couldn't find charmhelpers directory")
if not os.path.exists(NAGIOS_PLUGINS):
os.makedirs(NAGIOS_PLUGINS)
for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
if os.path.isfile(fname):
shutil.copy2(fname,
os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
def add_haproxy_checks(nrpe, unit_name):
"""
Add checks for each service in list
:param NRPE nrpe: NRPE object to add check to
:param str unit_name: Unit name to use in check description
"""
nrpe.add_check(
shortname='haproxy_servers',
description='Check HAProxy {%s}' % unit_name,
check_cmd='check_haproxy.sh')
nrpe.add_check(
shortname='haproxy_queue',
description='Check HAProxy queue depth {%s}' % unit_name,
check_cmd='check_haproxy_queue_depth.sh')
def remove_deprecated_check(nrpe, deprecated_services):
"""
Remove checks for deprecated services in list
:param nrpe: NRPE object to remove check from
:type nrpe: NRPE
:param deprecated_services: List of deprecated services that are removed
:type deprecated_services: list
"""
for dep_svc in deprecated_services:
log('Deprecated service: {}'.format(dep_svc))
nrpe.remove_check(shortname=dep_svc)
def add_deferred_restarts_check(nrpe):
"""
Add NRPE check for services with deferred restarts.
:param NRPE nrpe: NRPE object to add check to
"""
unit_name = local_unit().replace('/', '-')
shortname = unit_name + '_deferred_restarts'
check_cmd = 'check_deferred_restarts.py --application {}'.format(
application_name())
log('Adding deferred restarts nrpe check: {}'.format(shortname))
nrpe.add_check(
shortname=shortname,
description='Check deferred service restarts {}'.format(unit_name),
check_cmd=check_cmd)
def remove_deferred_restarts_check(nrpe):
"""
Remove NRPE check for services with deferred service restarts.
:param NRPE nrpe: NRPE object to remove check from
"""
unit_name = local_unit().replace('/', '-')
shortname = unit_name + '_deferred_restarts'
check_cmd = 'check_deferred_restarts.py --application {}'.format(
application_name())
log('Removing deferred restarts nrpe check: {}'.format(shortname))
nrpe.remove_check(
shortname=shortname,
description='Check deferred service restarts {}'.format(unit_name),
check_cmd=check_cmd)

View File

@@ -1,173 +0,0 @@
# Copyright 2014-2021 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
Functions for managing volumes in juju units. One volume is supported per unit.
Subordinates may have their own storage, provided it is on its own partition.
Configuration stanzas::
volume-ephemeral:
type: boolean
default: true
description: >
If false, a volume is mounted as specified in "volume-map"
If true, ephemeral storage will be used, meaning that log data
will only exist as long as the machine. YOU HAVE BEEN WARNED.
volume-map:
type: string
default: {}
description: >
YAML map of units to device names, e.g:
"{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
Service units will raise a configure-error if volume-ephemeral
is 'true' and no volume-map value is set. Use 'juju set' to set a
value and 'juju resolved' to complete configuration.
Usage::
from charmsupport.volumes import configure_volume, VolumeConfigurationError
from charmsupport.hookenv import log, ERROR
def post_mount_hook():
stop_service('myservice')
def post_mount_hook():
start_service('myservice')
if __name__ == '__main__':
try:
configure_volume(before_change=pre_mount_hook,
after_change=post_mount_hook)
except VolumeConfigurationError:
log('Storage could not be configured', ERROR)
'''
# XXX: Known limitations
# - fstab is neither consulted nor updated
import os
from charmhelpers.core import hookenv
from charmhelpers.core import host
import yaml
MOUNT_BASE = '/srv/juju/volumes'
class VolumeConfigurationError(Exception):
'''Volume configuration data is missing or invalid'''
pass
def get_config():
'''Gather and sanity-check volume configuration data'''
volume_config = {}
config = hookenv.config()
errors = False
if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
volume_config['ephemeral'] = True
else:
volume_config['ephemeral'] = False
try:
volume_map = yaml.safe_load(config.get('volume-map', '{}'))
except yaml.YAMLError as e:
hookenv.log("Error parsing YAML volume-map: {}".format(e),
hookenv.ERROR)
errors = True
if volume_map is None:
# probably an empty string
volume_map = {}
elif not isinstance(volume_map, dict):
hookenv.log("Volume-map should be a dictionary, not {}".format(
type(volume_map)))
errors = True
volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
if volume_config['device'] and volume_config['ephemeral']:
# asked for ephemeral storage but also defined a volume ID
hookenv.log('A volume is defined for this unit, but ephemeral '
'storage was requested', hookenv.ERROR)
errors = True
elif not volume_config['device'] and not volume_config['ephemeral']:
# asked for permanent storage but did not define volume ID
hookenv.log('Ephemeral storage was requested, but there is no volume '
'defined for this unit.', hookenv.ERROR)
errors = True
unit_mount_name = hookenv.local_unit().replace('/', '-')
volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
if errors:
return None
return volume_config
def mount_volume(config):
if os.path.exists(config['mountpoint']):
if not os.path.isdir(config['mountpoint']):
hookenv.log('Not a directory: {}'.format(config['mountpoint']))
raise VolumeConfigurationError()
else:
host.mkdir(config['mountpoint'])
if os.path.ismount(config['mountpoint']):
unmount_volume(config)
if not host.mount(config['device'], config['mountpoint'], persist=True):
raise VolumeConfigurationError()
def unmount_volume(config):
if os.path.ismount(config['mountpoint']):
if not host.umount(config['mountpoint'], persist=True):
raise VolumeConfigurationError()
def managed_mounts():
'''List of all mounted managed volumes'''
return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
def configure_volume(before_change=lambda: None, after_change=lambda: None):
'''Set up storage (or don't) according to the charm's volume configuration.
Returns the mount point or "ephemeral". before_change and after_change
are optional functions to be called if the volume configuration changes.
'''
config = get_config()
if not config:
hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
raise VolumeConfigurationError()
if config['ephemeral']:
if os.path.ismount(config['mountpoint']):
before_change()
unmount_volume(config)
after_change()
return 'ephemeral'
else:
# persistent storage
if os.path.ismount(config['mountpoint']):
mounts = dict(managed_mounts())
if mounts.get(config['mountpoint']) != config['device']:
before_change()
unmount_volume(config)
mount_volume(config)
after_change()
else:
before_change()
mount_volume(config)
after_change()
return config['mountpoint']

View File

@@ -1,13 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@@ -1,90 +0,0 @@
# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright 2012 Canonical Ltd.
#
# This file is sourced from lp:openstack-charm-helpers
#
# Authors:
# James Page <james.page@ubuntu.com>
# Adam Gandelman <adamg@ubuntu.com>
#
import os
from charmhelpers.core import host
from charmhelpers.core.hookenv import (
config as config_get,
relation_get,
relation_ids,
related_units as relation_list,
log,
INFO,
)
# This file contains the CA cert from the charms ssl_ca configuration
# option, in future the file name should be updated reflect that.
CONFIG_CA_CERT_FILE = 'keystone_juju_ca_cert'
def get_cert(cn=None):
# TODO: deal with multiple https endpoints via charm config
cert = config_get('ssl_cert')
key = config_get('ssl_key')
if not (cert and key):
log("Inspecting identity-service relations for SSL certificate.",
level=INFO)
cert = key = None
if cn:
ssl_cert_attr = 'ssl_cert_{}'.format(cn)
ssl_key_attr = 'ssl_key_{}'.format(cn)
else:
ssl_cert_attr = 'ssl_cert'
ssl_key_attr = 'ssl_key'
for r_id in relation_ids('identity-service'):
for unit in relation_list(r_id):
if not cert:
cert = relation_get(ssl_cert_attr,
rid=r_id, unit=unit)
if not key:
key = relation_get(ssl_key_attr,
rid=r_id, unit=unit)
return (cert, key)
def get_ca_cert():
ca_cert = config_get('ssl_ca')
if ca_cert is None:
log("Inspecting identity-service relations for CA SSL certificate.",
level=INFO)
for r_id in (relation_ids('identity-service') +
relation_ids('identity-credentials')):
for unit in relation_list(r_id):
if ca_cert is None:
ca_cert = relation_get('ca_cert',
rid=r_id, unit=unit)
return ca_cert
def retrieve_ca_cert(cert_file):
cert = None
if os.path.isfile(cert_file):
with open(cert_file, 'rb') as crt:
cert = crt.read()
return cert
def install_ca_cert(ca_cert):
host.install_ca_cert(ca_cert, CONFIG_CA_CERT_FILE)

View File

@@ -1,448 +0,0 @@
# Copyright 2014-2021 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright 2012 Canonical Ltd.
#
# Authors:
# James Page <james.page@ubuntu.com>
# Adam Gandelman <adamg@ubuntu.com>
#
"""
Helpers for clustering and determining "cluster leadership" and other
clustering-related helpers.
"""
import functools
import subprocess
import os
import time
from socket import gethostname as get_unit_hostname
from charmhelpers.core.hookenv import (
log,
relation_ids,
related_units as relation_list,
relation_get,
config as config_get,
INFO,
DEBUG,
WARNING,
unit_get,
is_leader as juju_is_leader,
status_set,
)
from charmhelpers.core.host import (
modulo_distribution,
)
from charmhelpers.core.decorators import (
retry_on_exception,
)
from charmhelpers.core.strutils import (
bool_from_string,
)
DC_RESOURCE_NAME = 'DC'
class HAIncompleteConfig(Exception):
pass
class HAIncorrectConfig(Exception):
pass
class CRMResourceNotFound(Exception):
pass
class CRMDCNotFound(Exception):
pass
def is_elected_leader(resource):
"""
Returns True if the charm executing this is the elected cluster leader.
It relies on two mechanisms to determine leadership:
1. If juju is sufficiently new and leadership election is supported,
the is_leader command will be used.
2. If the charm is part of a corosync cluster, call corosync to
determine leadership.
3. If the charm is not part of a corosync cluster, the leader is
determined as being "the alive unit with the lowest unit number". In
other words, the oldest surviving unit.
"""
try:
return juju_is_leader()
except NotImplementedError:
log('Juju leadership election feature not enabled'
', using fallback support',
level=WARNING)
if is_clustered():
if not is_crm_leader(resource):
log('Deferring action to CRM leader.', level=INFO)
return False
else:
peers = peer_units()
if peers and not oldest_peer(peers):
log('Deferring action to oldest service unit.', level=INFO)
return False
return True
def is_clustered():
for r_id in (relation_ids('ha') or []):
for unit in (relation_list(r_id) or []):
clustered = relation_get('clustered',
rid=r_id,
unit=unit)
if clustered:
return True
return False
def is_crm_dc():
"""
Determine leadership by querying the pacemaker Designated Controller
"""
cmd = ['crm', 'status']
try:
status = subprocess.check_output(
cmd, stderr=subprocess.STDOUT).decode('utf-8')
except subprocess.CalledProcessError as ex:
raise CRMDCNotFound(str(ex))
current_dc = ''
for line in status.split('\n'):
if line.startswith('Current DC'):
# Current DC: juju-lytrusty-machine-2 (168108163)
# - partition with quorum
current_dc = line.split(':')[1].split()[0]
if current_dc == get_unit_hostname():
return True
elif current_dc == 'NONE':
raise CRMDCNotFound('Current DC: NONE')
return False
@retry_on_exception(5, base_delay=2,
exc_type=(CRMResourceNotFound, CRMDCNotFound))
def is_crm_leader(resource, retry=False):
"""
Returns True if the charm calling this is the elected corosync leader,
as returned by calling the external "crm" command.
We allow this operation to be retried to avoid the possibility of getting a
false negative. See LP #1396246 for more info.
"""
if resource == DC_RESOURCE_NAME:
return is_crm_dc()
cmd = ['crm', 'resource', 'show', resource]
try:
status = subprocess.check_output(
cmd, stderr=subprocess.STDOUT).decode('utf-8')
except subprocess.CalledProcessError:
status = None
if status and get_unit_hostname() in status:
return True
if status and "resource %s is NOT running" % (resource) in status:
raise CRMResourceNotFound("CRM resource %s not found" % (resource))
return False
def is_leader(resource):
log("is_leader is deprecated. Please consider using is_crm_leader "
"instead.", level=WARNING)
return is_crm_leader(resource)
def peer_units(peer_relation="cluster"):
peers = []
for r_id in (relation_ids(peer_relation) or []):
for unit in (relation_list(r_id) or []):
peers.append(unit)
return peers
def peer_ips(peer_relation='cluster', addr_key='private-address'):
'''Return a dict of peers and their private-address'''
peers = {}
for r_id in relation_ids(peer_relation):
for unit in relation_list(r_id):
peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
return peers
def oldest_peer(peers):
"""Determines who the oldest peer is by comparing unit numbers."""
local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
for peer in peers:
remote_unit_no = int(peer.split('/')[1])
if remote_unit_no < local_unit_no:
return False
return True
def eligible_leader(resource):
log("eligible_leader is deprecated. Please consider using "
"is_elected_leader instead.", level=WARNING)
return is_elected_leader(resource)
def https():
'''
Determines whether enough data has been provided in configuration
or relation data to configure HTTPS
.
returns: boolean
'''
use_https = config_get('use-https')
if use_https and bool_from_string(use_https):
return True
if config_get('ssl_cert') and config_get('ssl_key'):
return True
for r_id in relation_ids('certificates'):
for unit in relation_list(r_id):
ca = relation_get('ca', rid=r_id, unit=unit)
if ca:
return True
for r_id in relation_ids('identity-service'):
for unit in relation_list(r_id):
# TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
rel_state = [
relation_get('https_keystone', rid=r_id, unit=unit),
relation_get('ca_cert', rid=r_id, unit=unit),
]
# NOTE: works around (LP: #1203241)
if (None not in rel_state) and ('' not in rel_state):
return True
return False
def determine_api_port(public_port, singlenode_mode=False):
'''
Determine correct API server listening port based on
existence of HTTPS reverse proxy and/or haproxy.
public_port: int: standard public port for given service
singlenode_mode: boolean: Shuffle ports when only a single unit is present
returns: int: the correct listening port for the API service
'''
i = 0
if singlenode_mode:
i += 1
elif len(peer_units()) > 0 or is_clustered():
i += 1
if https():
i += 1
return public_port - (i * 10)
def determine_apache_port(public_port, singlenode_mode=False):
'''
Description: Determine correct apache listening port based on public IP +
state of the cluster.
public_port: int: standard public port for given service
singlenode_mode: boolean: Shuffle ports when only a single unit is present
returns: int: the correct listening port for the HAProxy service
'''
i = 0
if singlenode_mode:
i += 1
elif len(peer_units()) > 0 or is_clustered():
i += 1
return public_port - (i * 10)
determine_apache_port_single = functools.partial(
determine_apache_port, singlenode_mode=True)
def get_hacluster_config(exclude_keys=None):
'''
Obtains all relevant configuration from charm configuration required
for initiating a relation to hacluster:
ha-bindiface, ha-mcastport, vip, os-internal-hostname,
os-admin-hostname, os-public-hostname, os-access-hostname
param: exclude_keys: list of setting key(s) to be excluded.
returns: dict: A dict containing settings keyed by setting name.
raises: HAIncompleteConfig if settings are missing or incorrect.
'''
settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'os-internal-hostname',
'os-admin-hostname', 'os-public-hostname', 'os-access-hostname']
conf = {}
for setting in settings:
if exclude_keys and setting in exclude_keys:
continue
conf[setting] = config_get(setting)
if not valid_hacluster_config():
raise HAIncorrectConfig('Insufficient or incorrect config data to '
'configure hacluster.')
return conf
def valid_hacluster_config():
'''
Check that either vip or dns-ha is set. If dns-ha then one of os-*-hostname
must be set.
Note: ha-bindiface and ha-macastport both have defaults and will always
be set. We only care that either vip or dns-ha is set.
:returns: boolean: valid config returns true.
raises: HAIncompatibileConfig if settings conflict.
raises: HAIncompleteConfig if settings are missing.
'''
vip = config_get('vip')
dns = config_get('dns-ha')
if not(bool(vip) ^ bool(dns)):
msg = ('HA: Either vip or dns-ha must be set but not both in order to '
'use high availability')
status_set('blocked', msg)
raise HAIncorrectConfig(msg)
# If dns-ha then one of os-*-hostname must be set
if dns:
dns_settings = ['os-internal-hostname', 'os-admin-hostname',
'os-public-hostname', 'os-access-hostname']
# At this point it is unknown if one or all of the possible
# network spaces are in HA. Validate at least one is set which is
# the minimum required.
for setting in dns_settings:
if config_get(setting):
log('DNS HA: At least one hostname is set {}: {}'
''.format(setting, config_get(setting)),
level=DEBUG)
return True
msg = ('DNS HA: At least one os-*-hostname(s) must be set to use '
'DNS HA')
status_set('blocked', msg)
raise HAIncompleteConfig(msg)
log('VIP HA: VIP is set {}'.format(vip), level=DEBUG)
return True
def canonical_url(configs, vip_setting='vip'):
'''
Returns the correct HTTP URL to this host given the state of HTTPS
configuration and hacluster.
:configs : OSTemplateRenderer: A config tempating object to inspect for
a complete https context.
:vip_setting: str: Setting in charm config that specifies
VIP address.
'''
scheme = 'http'
if 'https' in configs.complete_contexts():
scheme = 'https'
if is_clustered():
addr = config_get(vip_setting)
else:
addr = unit_get('private-address')
return '%s://%s' % (scheme, addr)
def distributed_wait(modulo=None, wait=None, operation_name='operation'):
''' Distribute operations by waiting based on modulo_distribution
If modulo and or wait are not set, check config_get for those values.
If config values are not set, default to modulo=3 and wait=30.
:param modulo: int The modulo number creates the group distribution
:param wait: int The constant time wait value
:param operation_name: string Operation name for status message
i.e. 'restart'
:side effect: Calls config_get()
:side effect: Calls log()
:side effect: Calls status_set()
:side effect: Calls time.sleep()
'''
if modulo is None:
modulo = config_get('modulo-nodes') or 3
if wait is None:
wait = config_get('known-wait') or 30
if juju_is_leader():
# The leader should never wait
calculated_wait = 0
else:
# non_zero_wait=True guarantees the non-leader who gets modulo 0
# will still wait
calculated_wait = modulo_distribution(modulo=modulo, wait=wait,
non_zero_wait=True)
msg = "Waiting {} seconds for {} ...".format(calculated_wait,
operation_name)
log(msg, DEBUG)
status_set('maintenance', msg)
time.sleep(calculated_wait)
def get_managed_services_and_ports(services, external_ports,
external_services=None,
port_conv_f=determine_apache_port_single):
"""Get the services and ports managed by this charm.
Return only the services and corresponding ports that are managed by this
charm. This excludes haproxy when there is a relation with hacluster. This
is because this charm passes responsibility for stopping and starting
haproxy to hacluster.
Similarly, if a relation with hacluster exists then the ports returned by
this method correspond to those managed by the apache server rather than
haproxy.
:param services: List of services.
:type services: List[str]
:param external_ports: List of ports managed by external services.
:type external_ports: List[int]
:param external_services: List of services to be removed if ha relation is
present.
:type external_services: List[str]
:param port_conv_f: Function to apply to ports to calculate the ports
managed by services controlled by this charm.
:type port_convert_func: f()
:returns: A tuple containing a list of services first followed by a list of
ports.
:rtype: Tuple[List[str], List[int]]
"""
if external_services is None:
external_services = ['haproxy']
if relation_ids('ha'):
for svc in external_services:
try:
services.remove(svc)
except ValueError:
pass
external_ports = [port_conv_f(p) for p in external_ports]
return services, external_ports

View File

@@ -1,38 +0,0 @@
# Juju charm-helpers hardening library
## Description
This library provides multiple implementations of system and application
hardening that conform to the standards of http://hardening.io/.
Current implementations include:
* OS
* SSH
* MySQL
* Apache
## Requirements
* Juju Charms
## Usage
1. Synchronise this library into your charm and add the harden() decorator
(from contrib.hardening.harden) to any functions or methods you want to use
to trigger hardening of your application/system.
2. Add a config option called 'harden' to your charm config.yaml and set it to
a space-delimited list of hardening modules you want to run e.g. "os ssh"
3. Override any config defaults (contrib.hardening.defaults) by adding a file
called hardening.yaml to your charm root containing the name(s) of the
modules whose settings you want override at root level and then any settings
with overrides e.g.
os:
general:
desktop_enable: True
4. Now just run your charm as usual and hardening will be applied each time the
hook runs.

View File

@@ -1,13 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@@ -1,17 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from os import path
TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')

View File

@@ -1,29 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.core.hookenv import (
log,
DEBUG,
)
from charmhelpers.contrib.hardening.apache.checks import config
def run_apache_checks():
log("Starting Apache hardening checks.", level=DEBUG)
checks = config.get_audits()
for check in checks:
log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
check.ensure_compliance()
log("Apache hardening checks complete.", level=DEBUG)

View File

@@ -1,101 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import re
import subprocess
from charmhelpers.core.hookenv import (
log,
INFO,
)
from charmhelpers.contrib.hardening.audits.file import (
FilePermissionAudit,
DirectoryPermissionAudit,
NoReadWriteForOther,
TemplatedFile,
DeletedFile
)
from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit
from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR
from charmhelpers.contrib.hardening import utils
def get_audits():
"""Get Apache hardening config audits.
:returns: dictionary of audits
"""
if subprocess.call(['which', 'apache2'], stdout=subprocess.PIPE) != 0:
log("Apache server does not appear to be installed on this node - "
"skipping apache hardening", level=INFO)
return []
context = ApacheConfContext()
settings = utils.get_settings('apache')
audits = [
FilePermissionAudit(paths=os.path.join(
settings['common']['apache_dir'], 'apache2.conf'),
user='root', group='root', mode=0o0640),
TemplatedFile(os.path.join(settings['common']['apache_dir'],
'mods-available/alias.conf'),
context,
TEMPLATES_DIR,
mode=0o0640,
user='root',
service_actions=[{'service': 'apache2',
'actions': ['restart']}]),
TemplatedFile(os.path.join(settings['common']['apache_dir'],
'conf-enabled/99-hardening.conf'),
context,
TEMPLATES_DIR,
mode=0o0640,
user='root',
service_actions=[{'service': 'apache2',
'actions': ['restart']}]),
DirectoryPermissionAudit(settings['common']['apache_dir'],
user='root',
group='root',
mode=0o0750),
DisabledModuleAudit(settings['hardening']['modules_to_disable']),
NoReadWriteForOther(settings['common']['apache_dir']),
DeletedFile(['/var/www/html/index.html'])
]
return audits
class ApacheConfContext(object):
"""Defines the set of key/value pairs to set in a apache config file.
This context, when called, will return a dictionary containing the
key/value pairs of setting to specify in the
/etc/apache/conf-enabled/hardening.conf file.
"""
def __call__(self):
settings = utils.get_settings('apache')
ctxt = settings['hardening']
out = subprocess.check_output(['apache2', '-v']).decode('utf-8')
ctxt['apache_version'] = re.search(r'.+version: Apache/(.+?)\s.+',
out).group(1)
ctxt['apache_icondir'] = '/usr/share/apache2/icons/'
return ctxt

View File

@@ -1,32 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
<Location / >
<LimitExcept {{ allowed_http_methods }} >
# http://httpd.apache.org/docs/2.4/upgrading.html
{% if apache_version > '2.2' -%}
Require all granted
{% else -%}
Order Allow,Deny
Deny from all
{% endif %}
</LimitExcept>
</Location>
<Directory />
Options -Indexes -FollowSymLinks
AllowOverride None
</Directory>
<Directory /var/www/>
Options -Indexes -FollowSymLinks
AllowOverride None
</Directory>
TraceEnable {{ traceenable }}
ServerTokens {{ servertokens }}
SSLHonorCipherOrder {{ honor_cipher_order }}
SSLCipherSuite {{ cipher_suite }}

View File

@@ -1,31 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
<IfModule alias_module>
#
# Aliases: Add here as many aliases as you need (with no limit). The format is
# Alias fakename realname
#
# Note that if you include a trailing / on fakename then the server will
# require it to be present in the URL. So "/icons" isn't aliased in this
# example, only "/icons/". If the fakename is slash-terminated, then the
# realname must also be slash terminated, and if the fakename omits the
# trailing slash, the realname must also omit it.
#
# We include the /icons/ alias for FancyIndexed directory listings. If
# you do not use FancyIndexing, you may comment this out.
#
Alias /icons/ "{{ apache_icondir }}/"
<Directory "{{ apache_icondir }}">
Options -Indexes -MultiViews -FollowSymLinks
AllowOverride None
{% if apache_version == '2.4' -%}
Require all granted
{% else -%}
Order allow,deny
Allow from all
{% endif %}
</Directory>
</IfModule>

View File

@@ -1,54 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class BaseAudit(object): # NO-QA
"""Base class for hardening checks.
The lifecycle of a hardening check is to first check to see if the system
is in compliance for the specified check. If it is not in compliance, the
check method will return a value which will be supplied to the.
"""
def __init__(self, *args, **kwargs):
self.unless = kwargs.get('unless', None)
super(BaseAudit, self).__init__()
def ensure_compliance(self):
"""Checks to see if the current hardening check is in compliance or
not.
If the check that is performed is not in compliance, then an exception
should be raised.
"""
pass
def _take_action(self):
"""Determines whether to perform the action or not.
Checks whether or not an action should be taken. This is determined by
the truthy value for the unless parameter. If unless is a callback
method, it will be invoked with no parameters in order to determine
whether or not the action should be taken. Otherwise, the truthy value
of the unless attribute will determine if the action should be
performed.
"""
# Do the action if there isn't an unless override.
if self.unless is None:
return True
# Invoke the callback if there is one.
if hasattr(self.unless, '__call__'):
return not self.unless()
return not self.unless

View File

@@ -1,101 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import subprocess
from charmhelpers.core.hookenv import (
log,
INFO,
ERROR,
)
from charmhelpers.contrib.hardening.audits import BaseAudit
class DisabledModuleAudit(BaseAudit):
"""Audits Apache2 modules.
Determines if the apache2 modules are enabled. If the modules are enabled
then they are removed in the ensure_compliance.
"""
def __init__(self, modules):
if modules is None:
self.modules = []
elif isinstance(modules, str):
self.modules = [modules]
else:
self.modules = modules
def ensure_compliance(self):
"""Ensures that the modules are not loaded."""
if not self.modules:
return
try:
loaded_modules = self._get_loaded_modules()
non_compliant_modules = []
for module in self.modules:
if module in loaded_modules:
log("Module '%s' is enabled but should not be." %
(module), level=INFO)
non_compliant_modules.append(module)
if len(non_compliant_modules) == 0:
return
for module in non_compliant_modules:
self._disable_module(module)
self._restart_apache()
except subprocess.CalledProcessError as e:
log('Error occurred auditing apache module compliance. '
'This may have been already reported. '
'Output is: %s' % e.output, level=ERROR)
@staticmethod
def _get_loaded_modules():
"""Returns the modules which are enabled in Apache."""
output = subprocess.check_output(['apache2ctl', '-M']).decode('utf-8')
modules = []
for line in output.splitlines():
# Each line of the enabled module output looks like:
# module_name (static|shared)
# Plus a header line at the top of the output which is stripped
# out by the regex.
matcher = re.search(r'^ (\S*)_module (\S*)', line)
if matcher:
modules.append(matcher.group(1))
return modules
@staticmethod
def _disable_module(module):
"""Disables the specified module in Apache."""
try:
subprocess.check_call(['a2dismod', module])
except subprocess.CalledProcessError as e:
# Note: catch error here to allow the attempt of disabling
# multiple modules in one go rather than failing after the
# first module fails.
log('Error occurred disabling module %s. '
'Output is: %s' % (module, e.output), level=ERROR)
@staticmethod
def _restart_apache():
"""Restarts the apache process"""
subprocess.check_output(['service', 'apache2', 'restart'])
@staticmethod
def is_ssl_enabled():
"""Check if SSL module is enabled or not"""
return 'ssl' in DisabledModuleAudit._get_loaded_modules()

View File

@@ -1,101 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.fetch import (
apt_cache,
apt_purge
)
from charmhelpers.core.hookenv import (
log,
DEBUG,
WARNING,
)
from charmhelpers.contrib.hardening.audits import BaseAudit
from charmhelpers.fetch import ubuntu_apt_pkg as apt_pkg
class AptConfig(BaseAudit):
def __init__(self, config, **kwargs):
self.config = config
def verify_config(self):
apt_pkg.init()
for cfg in self.config:
value = apt_pkg.config.get(cfg['key'], cfg.get('default', ''))
if value and value != cfg['expected']:
log("APT config '%s' has unexpected value '%s' "
"(expected='%s')" %
(cfg['key'], value, cfg['expected']), level=WARNING)
def ensure_compliance(self):
self.verify_config()
class RestrictedPackages(BaseAudit):
"""Class used to audit restricted packages on the system."""
def __init__(self, pkgs, **kwargs):
super(RestrictedPackages, self).__init__(**kwargs)
if isinstance(pkgs, str) or not hasattr(pkgs, '__iter__'):
self.pkgs = pkgs.split()
else:
self.pkgs = pkgs
def ensure_compliance(self):
cache = apt_cache()
for p in self.pkgs:
if p not in cache:
continue
pkg = cache[p]
if not self.is_virtual_package(pkg):
if not pkg.current_ver:
log("Package '%s' is not installed." % pkg.name,
level=DEBUG)
continue
else:
log("Restricted package '%s' is installed" % pkg.name,
level=WARNING)
self.delete_package(cache, pkg)
else:
log("Checking restricted virtual package '%s' provides" %
pkg.name, level=DEBUG)
self.delete_package(cache, pkg)
def delete_package(self, cache, pkg):
"""Deletes the package from the system.
Deletes the package form the system, properly handling virtual
packages.
:param cache: the apt cache
:param pkg: the package to remove
"""
if self.is_virtual_package(pkg):
log("Package '%s' appears to be virtual - purging provides" %
pkg.name, level=DEBUG)
for _p in pkg.provides_list:
self.delete_package(cache, _p[2].parent_pkg)
elif not pkg.current_ver:
log("Package '%s' not installed" % pkg.name, level=DEBUG)
return
else:
log("Purging package '%s'" % pkg.name, level=DEBUG)
apt_purge(pkg.name)
def is_virtual_package(self, pkg):
return (pkg.get('has_provides', False) and
not pkg.get('has_versions', False))

View File

@@ -1,549 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import grp
import os
import pwd
import re
from subprocess import (
CalledProcessError,
check_output,
check_call,
)
from traceback import format_exc
from stat import (
S_ISGID,
S_ISUID
)
from charmhelpers.core.hookenv import (
log,
DEBUG,
INFO,
WARNING,
ERROR,
)
from charmhelpers.core import unitdata
from charmhelpers.core.host import file_hash
from charmhelpers.contrib.hardening.audits import BaseAudit
from charmhelpers.contrib.hardening.templating import (
get_template_path,
render_and_write,
)
from charmhelpers.contrib.hardening import utils
class BaseFileAudit(BaseAudit):
"""Base class for file audits.
Provides api stubs for compliance check flow that must be used by any class
that implemented this one.
"""
def __init__(self, paths, always_comply=False, *args, **kwargs):
"""
:param paths: string path of list of paths of files we want to apply
compliance checks are criteria to.
:param always_comply: if true compliance criteria is always applied
else compliance is skipped for non-existent
paths.
"""
super(BaseFileAudit, self).__init__(*args, **kwargs)
self.always_comply = always_comply
if isinstance(paths, str) or not hasattr(paths, '__iter__'):
self.paths = [paths]
else:
self.paths = paths
def ensure_compliance(self):
"""Ensure that the all registered files comply to registered criteria.
"""
for p in self.paths:
if os.path.exists(p):
if self.is_compliant(p):
continue
log('File %s is not in compliance.' % p, level=INFO)
else:
if not self.always_comply:
log("Non-existent path '%s' - skipping compliance check"
% (p), level=INFO)
continue
if self._take_action():
log("Applying compliance criteria to '%s'" % (p), level=INFO)
self.comply(p)
def is_compliant(self, path):
"""Audits the path to see if it is compliance.
:param path: the path to the file that should be checked.
"""
raise NotImplementedError
def comply(self, path):
"""Enforces the compliance of a path.
:param path: the path to the file that should be enforced.
"""
raise NotImplementedError
@classmethod
def _get_stat(cls, path):
"""Returns the Posix st_stat information for the specified file path.
:param path: the path to get the st_stat information for.
:returns: an st_stat object for the path or None if the path doesn't
exist.
"""
return os.stat(path)
class FilePermissionAudit(BaseFileAudit):
"""Implements an audit for file permissions and ownership for a user.
This class implements functionality that ensures that a specific user/group
will own the file(s) specified and that the permissions specified are
applied properly to the file.
"""
def __init__(self, paths, user, group=None, mode=0o600, **kwargs):
self.user = user
self.group = group
self.mode = mode
super(FilePermissionAudit, self).__init__(paths, user, group, mode,
**kwargs)
@property
def user(self):
return self._user
@user.setter
def user(self, name):
try:
user = pwd.getpwnam(name)
except KeyError:
log('Unknown user %s' % name, level=ERROR)
user = None
self._user = user
@property
def group(self):
return self._group
@group.setter
def group(self, name):
try:
group = None
if name:
group = grp.getgrnam(name)
else:
group = grp.getgrgid(self.user.pw_gid)
except KeyError:
log('Unknown group %s' % name, level=ERROR)
self._group = group
def is_compliant(self, path):
"""Checks if the path is in compliance.
Used to determine if the path specified meets the necessary
requirements to be in compliance with the check itself.
:param path: the file path to check
:returns: True if the path is compliant, False otherwise.
"""
stat = self._get_stat(path)
user = self.user
group = self.group
compliant = True
if stat.st_uid != user.pw_uid or stat.st_gid != group.gr_gid:
log('File %s is not owned by %s:%s.' % (path, user.pw_name,
group.gr_name),
level=INFO)
compliant = False
# POSIX refers to the st_mode bits as corresponding to both the
# file type and file permission bits, where the least significant 12
# bits (o7777) are the suid (11), sgid (10), sticky bits (9), and the
# file permission bits (8-0)
perms = stat.st_mode & 0o7777
if perms != self.mode:
log('File %s has incorrect permissions, currently set to %s' %
(path, oct(stat.st_mode & 0o7777)), level=INFO)
compliant = False
return compliant
def comply(self, path):
"""Issues a chown and chmod to the file paths specified."""
utils.ensure_permissions(path, self.user.pw_name, self.group.gr_name,
self.mode)
class DirectoryPermissionAudit(FilePermissionAudit):
"""Performs a permission check for the specified directory path."""
def __init__(self, paths, user, group=None, mode=0o600,
recursive=True, **kwargs):
super(DirectoryPermissionAudit, self).__init__(paths, user, group,
mode, **kwargs)
self.recursive = recursive
def is_compliant(self, path):
"""Checks if the directory is compliant.
Used to determine if the path specified and all of its children
directories are in compliance with the check itself.
:param path: the directory path to check
:returns: True if the directory tree is compliant, otherwise False.
"""
if not os.path.isdir(path):
log('Path specified %s is not a directory.' % path, level=ERROR)
raise ValueError("%s is not a directory." % path)
if not self.recursive:
return super(DirectoryPermissionAudit, self).is_compliant(path)
compliant = True
for root, dirs, _ in os.walk(path):
if len(dirs) > 0:
continue
if not super(DirectoryPermissionAudit, self).is_compliant(root):
compliant = False
continue
return compliant
def comply(self, path):
for root, dirs, _ in os.walk(path):
if len(dirs) > 0:
super(DirectoryPermissionAudit, self).comply(root)
class ReadOnly(BaseFileAudit):
"""Audits that files and folders are read only."""
def __init__(self, paths, *args, **kwargs):
super(ReadOnly, self).__init__(paths=paths, *args, **kwargs)
def is_compliant(self, path):
try:
output = check_output(['find', path, '-perm', '-go+w',
'-type', 'f']).strip()
# The find above will find any files which have permission sets
# which allow too broad of write access. As such, the path is
# compliant if there is no output.
if output:
return False
return True
except CalledProcessError as e:
log('Error occurred checking finding writable files for %s. '
'Error information is: command %s failed with returncode '
'%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
format_exc(e)), level=ERROR)
return False
def comply(self, path):
try:
check_output(['chmod', 'go-w', '-R', path])
except CalledProcessError as e:
log('Error occurred removing writeable permissions for %s. '
'Error information is: command %s failed with returncode '
'%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
format_exc(e)), level=ERROR)
class NoReadWriteForOther(BaseFileAudit):
"""Ensures that the files found under the base path are readable or
writable by anyone other than the owner or the group.
"""
def __init__(self, paths):
super(NoReadWriteForOther, self).__init__(paths)
def is_compliant(self, path):
try:
cmd = ['find', path, '-perm', '-o+r', '-type', 'f', '-o',
'-perm', '-o+w', '-type', 'f']
output = check_output(cmd).strip()
# The find above here will find any files which have read or
# write permissions for other, meaning there is too broad of access
# to read/write the file. As such, the path is compliant if there's
# no output.
if output:
return False
return True
except CalledProcessError as e:
log('Error occurred while finding files which are readable or '
'writable to the world in %s. '
'Command output is: %s.' % (path, e.output), level=ERROR)
def comply(self, path):
try:
check_output(['chmod', '-R', 'o-rw', path])
except CalledProcessError as e:
log('Error occurred attempting to change modes of files under '
'path %s. Output of command is: %s' % (path, e.output))
class NoSUIDSGIDAudit(BaseFileAudit):
"""Audits that specified files do not have SUID/SGID bits set."""
def __init__(self, paths, *args, **kwargs):
super(NoSUIDSGIDAudit, self).__init__(paths=paths, *args, **kwargs)
def is_compliant(self, path):
stat = self._get_stat(path)
if (stat.st_mode & (S_ISGID | S_ISUID)) != 0:
return False
return True
def comply(self, path):
try:
log('Removing suid/sgid from %s.' % path, level=DEBUG)
check_output(['chmod', '-s', path])
except CalledProcessError as e:
log('Error occurred removing suid/sgid from %s.'
'Error information is: command %s failed with returncode '
'%d and output %s.\n%s' % (path, e.cmd, e.returncode, e.output,
format_exc(e)), level=ERROR)
class TemplatedFile(BaseFileAudit):
"""The TemplatedFileAudit audits the contents of a templated file.
This audit renders a file from a template, sets the appropriate file
permissions, then generates a hashsum with which to check the content
changed.
"""
def __init__(self, path, context, template_dir, mode, user='root',
group='root', service_actions=None, **kwargs):
self.context = context
self.user = user
self.group = group
self.mode = mode
self.template_dir = template_dir
self.service_actions = service_actions
super(TemplatedFile, self).__init__(paths=path, always_comply=True,
**kwargs)
def is_compliant(self, path):
"""Determines if the templated file is compliant.
A templated file is only compliant if it has not changed (as
determined by its sha256 hashsum) AND its file permissions are set
appropriately.
:param path: the path to check compliance.
"""
same_templates = self.templates_match(path)
same_content = self.contents_match(path)
same_permissions = self.permissions_match(path)
if same_content and same_permissions and same_templates:
return True
return False
def run_service_actions(self):
"""Run any actions on services requested."""
if not self.service_actions:
return
for svc_action in self.service_actions:
name = svc_action['service']
actions = svc_action['actions']
log("Running service '%s' actions '%s'" % (name, actions),
level=DEBUG)
for action in actions:
cmd = ['service', name, action]
try:
check_call(cmd)
except CalledProcessError as exc:
log("Service name='%s' action='%s' failed - %s" %
(name, action, exc), level=WARNING)
def comply(self, path):
"""Ensures the contents and the permissions of the file.
:param path: the path to correct
"""
dirname = os.path.dirname(path)
if not os.path.exists(dirname):
os.makedirs(dirname)
self.pre_write()
render_and_write(self.template_dir, path, self.context())
utils.ensure_permissions(path, self.user, self.group, self.mode)
self.run_service_actions()
self.save_checksum(path)
self.post_write()
def pre_write(self):
"""Invoked prior to writing the template."""
pass
def post_write(self):
"""Invoked after writing the template."""
pass
def templates_match(self, path):
"""Determines if the template files are the same.
The template file equality is determined by the hashsum of the
template files themselves. If there is no hashsum, then the content
cannot be sure to be the same so treat it as if they changed.
Otherwise, return whether or not the hashsums are the same.
:param path: the path to check
:returns: boolean
"""
template_path = get_template_path(self.template_dir, path)
key = 'hardening:template:%s' % template_path
template_checksum = file_hash(template_path)
kv = unitdata.kv()
stored_tmplt_checksum = kv.get(key)
if not stored_tmplt_checksum:
kv.set(key, template_checksum)
kv.flush()
log('Saved template checksum for %s.' % template_path,
level=DEBUG)
# Since we don't have a template checksum, then assume it doesn't
# match and return that the template is different.
return False
elif stored_tmplt_checksum != template_checksum:
kv.set(key, template_checksum)
kv.flush()
log('Updated template checksum for %s.' % template_path,
level=DEBUG)
return False
# Here the template hasn't changed based upon the calculated
# checksum of the template and what was previously stored.
return True
def contents_match(self, path):
"""Determines if the file content is the same.
This is determined by comparing hashsum of the file contents and
the saved hashsum. If there is no hashsum, then the content cannot
be sure to be the same so treat them as if they are not the same.
Otherwise, return True if the hashsums are the same, False if they
are not the same.
:param path: the file to check.
"""
checksum = file_hash(path)
kv = unitdata.kv()
stored_checksum = kv.get('hardening:%s' % path)
if not stored_checksum:
# If the checksum hasn't been generated, return False to ensure
# the file is written and the checksum stored.
log('Checksum for %s has not been calculated.' % path, level=DEBUG)
return False
elif stored_checksum != checksum:
log('Checksum mismatch for %s.' % path, level=DEBUG)
return False
return True
def permissions_match(self, path):
"""Determines if the file owner and permissions match.
:param path: the path to check.
"""
audit = FilePermissionAudit(path, self.user, self.group, self.mode)
return audit.is_compliant(path)
def save_checksum(self, path):
"""Calculates and saves the checksum for the path specified.
:param path: the path of the file to save the checksum.
"""
checksum = file_hash(path)
kv = unitdata.kv()
kv.set('hardening:%s' % path, checksum)
kv.flush()
class DeletedFile(BaseFileAudit):
"""Audit to ensure that a file is deleted."""
def __init__(self, paths):
super(DeletedFile, self).__init__(paths)
def is_compliant(self, path):
return not os.path.exists(path)
def comply(self, path):
os.remove(path)
class FileContentAudit(BaseFileAudit):
"""Audit the contents of a file."""
def __init__(self, paths, cases, **kwargs):
# Cases we expect to pass
self.pass_cases = cases.get('pass', [])
# Cases we expect to fail
self.fail_cases = cases.get('fail', [])
super(FileContentAudit, self).__init__(paths, **kwargs)
def is_compliant(self, path):
"""
Given a set of content matching cases i.e. tuple(regex, bool) where
bool value denotes whether or not regex is expected to match, check that
all cases match as expected with the contents of the file. Cases can be
expected to pass of fail.
:param path: Path of file to check.
:returns: Boolean value representing whether or not all cases are
found to be compliant.
"""
log("Auditing contents of file '%s'" % (path), level=DEBUG)
with open(path, 'r') as fd:
contents = fd.read()
matches = 0
for pattern in self.pass_cases:
key = re.compile(pattern, flags=re.MULTILINE)
results = re.search(key, contents)
if results:
matches += 1
else:
log("Pattern '%s' was expected to pass but instead it failed"
% (pattern), level=WARNING)
for pattern in self.fail_cases:
key = re.compile(pattern, flags=re.MULTILINE)
results = re.search(key, contents)
if not results:
matches += 1
else:
log("Pattern '%s' was expected to fail but instead it passed"
% (pattern), level=WARNING)
total = len(self.pass_cases) + len(self.fail_cases)
log("Checked %s cases and %s passed" % (total, matches), level=DEBUG)
return matches == total
def comply(self, *args, **kwargs):
"""NOOP since we just issue warnings. This is to avoid the
NotImplememtedError.
"""
log("Not applying any compliance criteria, only checks.", level=INFO)

View File

@@ -1,16 +0,0 @@
# NOTE: this file contains the default configuration for the 'apache' hardening
# code. If you want to override any settings you must add them to a file
# called hardening.yaml in the root directory of your charm using the
# name 'apache' as the root key followed by any of the following with new
# values.
common:
apache_dir: '/etc/apache2'
hardening:
traceenable: 'off'
allowed_http_methods: "GET POST"
modules_to_disable: [ cgi, cgid ]
servertokens: 'Prod'
honor_cipher_order: 'on'
cipher_suite: 'ALL:+MEDIUM:+HIGH:!LOW:!MD5:!RC4:!eNULL:!aNULL:!3DES'

View File

@@ -1,12 +0,0 @@
# NOTE: this schema must contain all valid keys from it's associated defaults
# file. It is used to validate user-provided overrides.
common:
apache_dir:
traceenable:
hardening:
allowed_http_methods:
modules_to_disable:
servertokens:
honor_cipher_order:
cipher_suite:

View File

@@ -1,38 +0,0 @@
# NOTE: this file contains the default configuration for the 'mysql' hardening
# code. If you want to override any settings you must add them to a file
# called hardening.yaml in the root directory of your charm using the
# name 'mysql' as the root key followed by any of the following with new
# values.
hardening:
mysql-conf: /etc/mysql/my.cnf
hardening-conf: /etc/mysql/conf.d/hardening.cnf
security:
# @see http://www.symantec.com/connect/articles/securing-mysql-step-step
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_chroot
chroot: None
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_safe-user-create
safe-user-create: 1
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_secure-auth
secure-auth: 1
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_symbolic-links
skip-symbolic-links: 1
# @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_skip-show-database
skip-show-database: True
# @see http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_local_infile
local-infile: 0
# @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_allow-suspicious-udfs
allow-suspicious-udfs: 0
# @see https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_automatic_sp_privileges
automatic-sp-privileges: 0
# @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_secure-file-priv
secure-file-priv: /tmp

View File

@@ -1,15 +0,0 @@
# NOTE: this schema must contain all valid keys from it's associated defaults
# file. It is used to validate user-provided overrides.
hardening:
mysql-conf:
hardening-conf:
security:
chroot:
safe-user-create:
secure-auth:
skip-symbolic-links:
skip-show-database:
local-infile:
allow-suspicious-udfs:
automatic-sp-privileges:
secure-file-priv:

View File

@@ -1,68 +0,0 @@
# NOTE: this file contains the default configuration for the 'os' hardening
# code. If you want to override any settings you must add them to a file
# called hardening.yaml in the root directory of your charm using the
# name 'os' as the root key followed by any of the following with new
# values.
general:
desktop_enable: False # (type:boolean)
environment:
extra_user_paths: []
umask: 027
root_path: /
auth:
pw_max_age: 60
# discourage password cycling
pw_min_age: 7
retries: 5
lockout_time: 600
timeout: 60
allow_homeless: False # (type:boolean)
pam_passwdqc_enable: True # (type:boolean)
pam_passwdqc_options: 'min=disabled,disabled,16,12,8'
root_ttys:
console
tty1
tty2
tty3
tty4
tty5
tty6
uid_min: 1000
gid_min: 1000
sys_uid_min: 100
sys_uid_max: 999
sys_gid_min: 100
sys_gid_max: 999
chfn_restrict:
security:
users_allow: []
suid_sgid_enforce: True # (type:boolean)
# user-defined blacklist and whitelist
suid_sgid_blacklist: []
suid_sgid_whitelist: []
# if this is True, remove any suid/sgid bits from files that were not in the whitelist
suid_sgid_dry_run_on_unknown: False # (type:boolean)
suid_sgid_remove_from_unknown: False # (type:boolean)
# remove packages with known issues
packages_clean: True # (type:boolean)
packages_list:
xinetd
inetd
ypserv
telnet-server
rsh-server
rsync
kernel_enable_module_loading: True # (type:boolean)
kernel_enable_core_dump: False # (type:boolean)
ssh_tmout: 300
sysctl:
kernel_secure_sysrq: 244 # 4 + 16 + 32 + 64 + 128
kernel_enable_sysrq: False # (type:boolean)
forwarding: False # (type:boolean)
ipv6_enable: False # (type:boolean)
arp_restricted: True # (type:boolean)

View File

@@ -1,43 +0,0 @@
# NOTE: this schema must contain all valid keys from it's associated defaults
# file. It is used to validate user-provided overrides.
general:
desktop_enable:
environment:
extra_user_paths:
umask:
root_path:
auth:
pw_max_age:
pw_min_age:
retries:
lockout_time:
timeout:
allow_homeless:
pam_passwdqc_enable:
pam_passwdqc_options:
root_ttys:
uid_min:
gid_min:
sys_uid_min:
sys_uid_max:
sys_gid_min:
sys_gid_max:
chfn_restrict:
security:
users_allow:
suid_sgid_enforce:
suid_sgid_blacklist:
suid_sgid_whitelist:
suid_sgid_dry_run_on_unknown:
suid_sgid_remove_from_unknown:
packages_clean:
packages_list:
kernel_enable_module_loading:
kernel_enable_core_dump:
ssh_tmout:
sysctl:
kernel_secure_sysrq:
kernel_enable_sysrq:
forwarding:
ipv6_enable:
arp_restricted:

View File

@@ -1,49 +0,0 @@
# NOTE: this file contains the default configuration for the 'ssh' hardening
# code. If you want to override any settings you must add them to a file
# called hardening.yaml in the root directory of your charm using the
# name 'ssh' as the root key followed by any of the following with new
# values.
common:
service_name: 'ssh'
network_ipv6_enable: False # (type:boolean)
ports: [22]
remote_hosts: []
client:
package: 'openssh-client'
cbc_required: False # (type:boolean)
weak_hmac: False # (type:boolean)
weak_kex: False # (type:boolean)
roaming: False
password_authentication: 'no'
server:
host_key_files: ['/etc/ssh/ssh_host_rsa_key', '/etc/ssh/ssh_host_dsa_key',
'/etc/ssh/ssh_host_ecdsa_key']
cbc_required: False # (type:boolean)
weak_hmac: False # (type:boolean)
weak_kex: False # (type:boolean)
allow_root_with_key: False # (type:boolean)
allow_tcp_forwarding: 'no'
allow_agent_forwarding: 'no'
allow_x11_forwarding: 'no'
use_privilege_separation: 'sandbox'
listen_to: ['0.0.0.0']
use_pam: 'no'
package: 'openssh-server'
password_authentication: 'no'
alive_interval: '600'
alive_count: '3'
sftp_enable: False # (type:boolean)
sftp_group: 'sftponly'
sftp_chroot: '/home/%u'
deny_users: []
allow_users: []
deny_groups: []
allow_groups: []
print_motd: 'no'
print_last_log: 'no'
use_dns: 'no'
max_auth_tries: 2
max_sessions: 10

View File

@@ -1,42 +0,0 @@
# NOTE: this schema must contain all valid keys from it's associated defaults
# file. It is used to validate user-provided overrides.
common:
service_name:
network_ipv6_enable:
ports:
remote_hosts:
client:
package:
cbc_required:
weak_hmac:
weak_kex:
roaming:
password_authentication:
server:
host_key_files:
cbc_required:
weak_hmac:
weak_kex:
allow_root_with_key:
allow_tcp_forwarding:
allow_agent_forwarding:
allow_x11_forwarding:
use_privilege_separation:
listen_to:
use_pam:
package:
password_authentication:
alive_interval:
alive_count:
sftp_enable:
sftp_group:
sftp_chroot:
deny_users:
allow_users:
deny_groups:
allow_groups:
print_motd:
print_last_log:
use_dns:
max_auth_tries:
max_sessions:

View File

@@ -1,93 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import OrderedDict
from charmhelpers.core.hookenv import (
config,
log,
DEBUG,
WARNING,
)
from charmhelpers.contrib.hardening.host.checks import run_os_checks
from charmhelpers.contrib.hardening.ssh.checks import run_ssh_checks
from charmhelpers.contrib.hardening.mysql.checks import run_mysql_checks
from charmhelpers.contrib.hardening.apache.checks import run_apache_checks
_DISABLE_HARDENING_FOR_UNIT_TEST = False
def harden(overrides=None):
"""Hardening decorator.
This is the main entry point for running the hardening stack. In order to
run modules of the stack you must add this decorator to charm hook(s) and
ensure that your charm config.yaml contains the 'harden' option set to
one or more of the supported modules. Setting these will cause the
corresponding hardening code to be run when the hook fires.
This decorator can and should be applied to more than one hook or function
such that hardening modules are called multiple times. This is because
subsequent calls will perform auditing checks that will report any changes
to resources hardened by the first run (and possibly perform compliance
actions as a result of any detected infractions).
:param overrides: Optional list of stack modules used to override those
provided with 'harden' config.
:returns: Returns value returned by decorated function once executed.
"""
if overrides is None:
overrides = []
def _harden_inner1(f):
_logged = False
def _harden_inner2(*args, **kwargs):
# knock out hardening via a config var; normally it won't get
# disabled.
nonlocal _logged
if _DISABLE_HARDENING_FOR_UNIT_TEST:
return f(*args, **kwargs)
if not _logged:
log("Hardening function '%s'" % (f.__name__), level=DEBUG)
_logged = True
RUN_CATALOG = OrderedDict([('os', run_os_checks),
('ssh', run_ssh_checks),
('mysql', run_mysql_checks),
('apache', run_apache_checks)])
enabled = overrides[:] or (config("harden") or "").split()
if enabled:
modules_to_run = []
# modules will always be performed in the following order
for module, func in RUN_CATALOG.items():
if module in enabled:
enabled.remove(module)
modules_to_run.append(func)
if enabled:
log("Unknown hardening modules '%s' - ignoring" %
(', '.join(enabled)), level=WARNING)
for hardener in modules_to_run:
log("Executing hardening module '%s'" %
(hardener.__name__), level=DEBUG)
hardener()
else:
log("No hardening applied to '%s'" % (f.__name__), level=DEBUG)
return f(*args, **kwargs)
return _harden_inner2
return _harden_inner1

View File

@@ -1,17 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from os import path
TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')

View File

@@ -1,48 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.core.hookenv import (
log,
DEBUG,
)
from charmhelpers.contrib.hardening.host.checks import (
apt,
limits,
login,
minimize_access,
pam,
profile,
securetty,
suid_sgid,
sysctl
)
def run_os_checks():
log("Starting OS hardening checks.", level=DEBUG)
checks = apt.get_audits()
checks.extend(limits.get_audits())
checks.extend(login.get_audits())
checks.extend(minimize_access.get_audits())
checks.extend(pam.get_audits())
checks.extend(profile.get_audits())
checks.extend(securetty.get_audits())
checks.extend(suid_sgid.get_audits())
checks.extend(sysctl.get_audits())
for check in checks:
log("Running '%s' check" % (check.__class__.__name__), level=DEBUG)
check.ensure_compliance()
log("OS hardening checks complete.", level=DEBUG)

View File

@@ -1,37 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.contrib.hardening.utils import get_settings
from charmhelpers.contrib.hardening.audits.apt import (
AptConfig,
RestrictedPackages,
)
def get_audits():
"""Get OS hardening apt audits.
:returns: dictionary of audits
"""
audits = [AptConfig([{'key': 'APT::Get::AllowUnauthenticated',
'expected': 'false'}])]
settings = get_settings('os')
clean_packages = settings['security']['packages_clean']
if clean_packages:
security_packages = settings['security']['packages_list']
if security_packages:
audits.append(RestrictedPackages(security_packages))
return audits

View File

@@ -1,53 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.contrib.hardening.audits.file import (
DirectoryPermissionAudit,
TemplatedFile,
)
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
from charmhelpers.contrib.hardening import utils
def get_audits():
"""Get OS hardening security limits audits.
:returns: dictionary of audits
"""
audits = []
settings = utils.get_settings('os')
# Ensure that the /etc/security/limits.d directory is only writable
# by the root user, but others can execute and read.
audits.append(DirectoryPermissionAudit('/etc/security/limits.d',
user='root', group='root',
mode=0o755))
# If core dumps are not enabled, then don't allow core dumps to be
# created as they may contain sensitive information.
if not settings['security']['kernel_enable_core_dump']:
audits.append(TemplatedFile('/etc/security/limits.d/10.hardcore.conf',
SecurityLimitsContext(),
template_dir=TEMPLATES_DIR,
user='root', group='root', mode=0o0440))
return audits
class SecurityLimitsContext(object):
def __call__(self):
settings = utils.get_settings('os')
ctxt = {'disable_core_dump':
not settings['security']['kernel_enable_core_dump']}
return ctxt

View File

@@ -1,63 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.contrib.hardening.audits.file import TemplatedFile
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
from charmhelpers.contrib.hardening import utils
def get_audits():
"""Get OS hardening login.defs audits.
:returns: dictionary of audits
"""
audits = [TemplatedFile('/etc/login.defs', LoginContext(),
template_dir=TEMPLATES_DIR,
user='root', group='root', mode=0o0444)]
return audits
class LoginContext(object):
def __call__(self):
settings = utils.get_settings('os')
# Octal numbers in yaml end up being turned into decimal,
# so check if the umask is entered as a string (e.g. '027')
# or as an octal umask as we know it (e.g. 002). If its not
# a string assume it to be octal and turn it into an octal
# string.
umask = settings['environment']['umask']
if not isinstance(umask, str):
umask = '%s' % oct(umask)
ctxt = {
'additional_user_paths':
settings['environment']['extra_user_paths'],
'umask': umask,
'pwd_max_age': settings['auth']['pw_max_age'],
'pwd_min_age': settings['auth']['pw_min_age'],
'uid_min': settings['auth']['uid_min'],
'sys_uid_min': settings['auth']['sys_uid_min'],
'sys_uid_max': settings['auth']['sys_uid_max'],
'gid_min': settings['auth']['gid_min'],
'sys_gid_min': settings['auth']['sys_gid_min'],
'sys_gid_max': settings['auth']['sys_gid_max'],
'login_retries': settings['auth']['retries'],
'login_timeout': settings['auth']['timeout'],
'chfn_restrict': settings['auth']['chfn_restrict'],
'allow_login_without_home': settings['auth']['allow_homeless']
}
return ctxt

View File

@@ -1,50 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.contrib.hardening.audits.file import (
FilePermissionAudit,
ReadOnly,
)
from charmhelpers.contrib.hardening import utils
def get_audits():
"""Get OS hardening access audits.
:returns: dictionary of audits
"""
audits = []
settings = utils.get_settings('os')
# Remove write permissions from $PATH folders for all regular users.
# This prevents changing system-wide commands from normal users.
path_folders = {'/usr/local/sbin',
'/usr/local/bin',
'/usr/sbin',
'/usr/bin',
'/bin'}
extra_user_paths = settings['environment']['extra_user_paths']
path_folders.update(extra_user_paths)
audits.append(ReadOnly(path_folders))
# Only allow the root user to have access to the shadow file.
audits.append(FilePermissionAudit('/etc/shadow', 'root', 'root', 0o0600))
if 'change_user' not in settings['security']['users_allow']:
# su should only be accessible to user and group root, unless it is
# expressly defined to allow users to change to root via the
# security_users_allow config option.
audits.append(FilePermissionAudit('/bin/su', 'root', 'root', 0o750))
return audits

View File

@@ -1,132 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from subprocess import (
check_output,
CalledProcessError,
)
from charmhelpers.core.hookenv import (
log,
DEBUG,
ERROR,
)
from charmhelpers.fetch import (
apt_install,
apt_purge,
apt_update,
)
from charmhelpers.contrib.hardening.audits.file import (
TemplatedFile,
DeletedFile,
)
from charmhelpers.contrib.hardening import utils
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
def get_audits():
"""Get OS hardening PAM authentication audits.
:returns: dictionary of audits
"""
audits = []
settings = utils.get_settings('os')
if settings['auth']['pam_passwdqc_enable']:
audits.append(PasswdqcPAM('/etc/passwdqc.conf'))
if settings['auth']['retries']:
audits.append(Tally2PAM('/usr/share/pam-configs/tally2'))
else:
audits.append(DeletedFile('/usr/share/pam-configs/tally2'))
return audits
class PasswdqcPAMContext(object):
def __call__(self):
ctxt = {}
settings = utils.get_settings('os')
ctxt['auth_pam_passwdqc_options'] = \
settings['auth']['pam_passwdqc_options']
return ctxt
class PasswdqcPAM(TemplatedFile):
"""The PAM Audit verifies the linux PAM settings."""
def __init__(self, path):
super(PasswdqcPAM, self).__init__(path=path,
template_dir=TEMPLATES_DIR,
context=PasswdqcPAMContext(),
user='root',
group='root',
mode=0o0640)
def pre_write(self):
# Always remove?
for pkg in ['libpam-ccreds', 'libpam-cracklib']:
log("Purging package '%s'" % pkg, level=DEBUG),
apt_purge(pkg)
apt_update(fatal=True)
for pkg in ['libpam-passwdqc']:
log("Installing package '%s'" % pkg, level=DEBUG),
apt_install(pkg)
def post_write(self):
"""Updates the PAM configuration after the file has been written"""
try:
check_output(['pam-auth-update', '--package'])
except CalledProcessError as e:
log('Error calling pam-auth-update: %s' % e, level=ERROR)
class Tally2PAMContext(object):
def __call__(self):
ctxt = {}
settings = utils.get_settings('os')
ctxt['auth_lockout_time'] = settings['auth']['lockout_time']
ctxt['auth_retries'] = settings['auth']['retries']
return ctxt
class Tally2PAM(TemplatedFile):
"""The PAM Audit verifies the linux PAM settings."""
def __init__(self, path):
super(Tally2PAM, self).__init__(path=path,
template_dir=TEMPLATES_DIR,
context=Tally2PAMContext(),
user='root',
group='root',
mode=0o0640)
def pre_write(self):
# Always remove?
apt_purge('libpam-ccreds')
apt_update(fatal=True)
apt_install('libpam-modules')
def post_write(self):
"""Updates the PAM configuration after the file has been written"""
try:
check_output(['pam-auth-update', '--package'])
except CalledProcessError as e:
log('Error calling pam-auth-update: %s' % e, level=ERROR)

View File

@@ -1,49 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.contrib.hardening.audits.file import TemplatedFile
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
from charmhelpers.contrib.hardening import utils
def get_audits():
"""Get OS hardening profile audits.
:returns: dictionary of audits
"""
audits = []
settings = utils.get_settings('os')
# If core dumps are not enabled, then don't allow core dumps to be
# created as they may contain sensitive information.
if not settings['security']['kernel_enable_core_dump']:
audits.append(TemplatedFile('/etc/profile.d/pinerolo_profile.sh',
ProfileContext(),
template_dir=TEMPLATES_DIR,
mode=0o0755, user='root', group='root'))
if settings['security']['ssh_tmout']:
audits.append(TemplatedFile('/etc/profile.d/99-hardening.sh',
ProfileContext(),
template_dir=TEMPLATES_DIR,
mode=0o0644, user='root', group='root'))
return audits
class ProfileContext(object):
def __call__(self):
settings = utils.get_settings('os')
ctxt = {'ssh_tmout':
settings['security']['ssh_tmout']}
return ctxt

View File

@@ -1,37 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.contrib.hardening.audits.file import TemplatedFile
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
from charmhelpers.contrib.hardening import utils
def get_audits():
"""Get OS hardening Secure TTY audits.
:returns: dictionary of audits
"""
audits = []
audits.append(TemplatedFile('/etc/securetty', SecureTTYContext(),
template_dir=TEMPLATES_DIR,
mode=0o0400, user='root', group='root'))
return audits
class SecureTTYContext(object):
def __call__(self):
settings = utils.get_settings('os')
ctxt = {'ttys': settings['auth']['root_ttys']}
return ctxt

View File

@@ -1,129 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import subprocess
from charmhelpers.core.hookenv import (
log,
INFO,
)
from charmhelpers.contrib.hardening.audits.file import NoSUIDSGIDAudit
from charmhelpers.contrib.hardening import utils
BLACKLIST = ['/usr/bin/rcp', '/usr/bin/rlogin', '/usr/bin/rsh',
'/usr/libexec/openssh/ssh-keysign',
'/usr/lib/openssh/ssh-keysign',
'/sbin/netreport',
'/usr/sbin/usernetctl',
'/usr/sbin/userisdnctl',
'/usr/sbin/pppd',
'/usr/bin/lockfile',
'/usr/bin/mail-lock',
'/usr/bin/mail-unlock',
'/usr/bin/mail-touchlock',
'/usr/bin/dotlockfile',
'/usr/bin/arping',
'/usr/sbin/uuidd',
'/usr/bin/mtr',
'/usr/lib/evolution/camel-lock-helper-1.2',
'/usr/lib/pt_chown',
'/usr/lib/eject/dmcrypt-get-device',
'/usr/lib/mc/cons.saver']
WHITELIST = ['/bin/mount', '/bin/ping', '/bin/su', '/bin/umount',
'/sbin/pam_timestamp_check', '/sbin/unix_chkpwd', '/usr/bin/at',
'/usr/bin/gpasswd', '/usr/bin/locate', '/usr/bin/newgrp',
'/usr/bin/passwd', '/usr/bin/ssh-agent',
'/usr/libexec/utempter/utempter', '/usr/sbin/lockdev',
'/usr/sbin/sendmail.sendmail', '/usr/bin/expiry',
'/bin/ping6', '/usr/bin/traceroute6.iputils',
'/sbin/mount.nfs', '/sbin/umount.nfs',
'/sbin/mount.nfs4', '/sbin/umount.nfs4',
'/usr/bin/crontab',
'/usr/bin/wall', '/usr/bin/write',
'/usr/bin/screen',
'/usr/bin/mlocate',
'/usr/bin/chage', '/usr/bin/chfn', '/usr/bin/chsh',
'/bin/fusermount',
'/usr/bin/pkexec',
'/usr/bin/sudo', '/usr/bin/sudoedit',
'/usr/sbin/postdrop', '/usr/sbin/postqueue',
'/usr/sbin/suexec',
'/usr/lib/squid/ncsa_auth', '/usr/lib/squid/pam_auth',
'/usr/kerberos/bin/ksu',
'/usr/sbin/ccreds_validate',
'/usr/bin/Xorg',
'/usr/bin/X',
'/usr/lib/dbus-1.0/dbus-daemon-launch-helper',
'/usr/lib/vte/gnome-pty-helper',
'/usr/lib/libvte9/gnome-pty-helper',
'/usr/lib/libvte-2.90-9/gnome-pty-helper']
def get_audits():
"""Get OS hardening suid/sgid audits.
:returns: dictionary of audits
"""
checks = []
settings = utils.get_settings('os')
if not settings['security']['suid_sgid_enforce']:
log("Skipping suid/sgid hardening", level=INFO)
return checks
# Build the blacklist and whitelist of files for suid/sgid checks.
# There are a total of 4 lists:
# 1. the system blacklist
# 2. the system whitelist
# 3. the user blacklist
# 4. the user whitelist
#
# The blacklist is the set of paths which should NOT have the suid/sgid bit
# set and the whitelist is the set of paths which MAY have the suid/sgid
# bit setl. The user whitelist/blacklist effectively override the system
# whitelist/blacklist.
u_b = settings['security']['suid_sgid_blacklist']
u_w = settings['security']['suid_sgid_whitelist']
blacklist = set(BLACKLIST) - set(u_w + u_b)
whitelist = set(WHITELIST) - set(u_b + u_w)
checks.append(NoSUIDSGIDAudit(blacklist))
dry_run = settings['security']['suid_sgid_dry_run_on_unknown']
if settings['security']['suid_sgid_remove_from_unknown'] or dry_run:
# If the policy is a dry_run (e.g. complain only) or remove unknown
# suid/sgid bits then find all of the paths which have the suid/sgid
# bit set and then remove the whitelisted paths.
root_path = settings['environment']['root_path']
unknown_paths = find_paths_with_suid_sgid(root_path) - set(whitelist)
checks.append(NoSUIDSGIDAudit(unknown_paths, unless=dry_run))
return checks
def find_paths_with_suid_sgid(root_path):
"""Finds all paths/files which have an suid/sgid bit enabled.
Starting with the root_path, this will recursively find all paths which
have an suid or sgid bit set.
"""
cmd = ['find', root_path, '-perm', '-4000', '-o', '-perm', '-2000',
'-type', 'f', '!', '-path', '/proc/*', '-print']
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, _ = p.communicate()
return set(out.split('\n'))

View File

@@ -1,208 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import platform
import re
import subprocess
from charmhelpers.core.hookenv import (
log,
INFO,
WARNING,
)
from charmhelpers.contrib.hardening import utils
from charmhelpers.contrib.hardening.audits.file import (
FilePermissionAudit,
TemplatedFile,
)
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
SYSCTL_DEFAULTS = """net.ipv4.ip_forward=%(net_ipv4_ip_forward)s
net.ipv6.conf.all.forwarding=%(net_ipv6_conf_all_forwarding)s
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.icmp_ignore_bogus_error_responses=1
net.ipv4.icmp_ratelimit=100
net.ipv4.icmp_ratemask=88089
net.ipv6.conf.all.disable_ipv6=%(net_ipv6_conf_all_disable_ipv6)s
net.ipv4.tcp_timestamps=%(net_ipv4_tcp_timestamps)s
net.ipv4.conf.all.arp_ignore=%(net_ipv4_conf_all_arp_ignore)s
net.ipv4.conf.all.arp_announce=%(net_ipv4_conf_all_arp_announce)s
net.ipv4.tcp_rfc1337=1
net.ipv4.tcp_syncookies=1
net.ipv4.conf.all.shared_media=1
net.ipv4.conf.default.shared_media=1
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
net.ipv4.conf.all.secure_redirects=0
net.ipv4.conf.default.secure_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.send_redirects=0
net.ipv4.conf.all.log_martians=0
net.ipv6.conf.default.router_solicitations=0
net.ipv6.conf.default.accept_ra_rtr_pref=0
net.ipv6.conf.default.accept_ra_pinfo=0
net.ipv6.conf.default.accept_ra_defrtr=0
net.ipv6.conf.default.autoconf=0
net.ipv6.conf.default.dad_transmits=0
net.ipv6.conf.default.max_addresses=1
net.ipv6.conf.all.accept_ra=0
net.ipv6.conf.default.accept_ra=0
kernel.modules_disabled=%(kernel_modules_disabled)s
kernel.sysrq=%(kernel_sysrq)s
fs.suid_dumpable=%(fs_suid_dumpable)s
kernel.randomize_va_space=2
"""
def get_audits():
"""Get OS hardening sysctl audits.
:returns: dictionary of audits
"""
audits = []
settings = utils.get_settings('os')
# Apply the sysctl settings which are configured to be applied.
audits.append(SysctlConf())
# Make sure that only root has access to the sysctl.conf file, and
# that it is read-only.
audits.append(FilePermissionAudit('/etc/sysctl.conf',
user='root',
group='root', mode=0o0440))
# If module loading is not enabled, then ensure that the modules
# file has the appropriate permissions and rebuild the initramfs
if not settings['security']['kernel_enable_module_loading']:
audits.append(ModulesTemplate())
return audits
class ModulesContext(object):
def __call__(self):
settings = utils.get_settings('os')
with open('/proc/cpuinfo', 'r') as fd:
cpuinfo = fd.readlines()
for line in cpuinfo:
match = re.search(r"^vendor_id\s+:\s+(.+)", line)
if match:
vendor = match.group(1)
if vendor == "GenuineIntel":
vendor = "intel"
elif vendor == "AuthenticAMD":
vendor = "amd"
ctxt = {'arch': platform.processor(),
'cpuVendor': vendor,
'desktop_enable': settings['general']['desktop_enable']}
return ctxt
class ModulesTemplate(object):
def __init__(self):
super(ModulesTemplate, self).__init__('/etc/initramfs-tools/modules',
ModulesContext(),
templates_dir=TEMPLATES_DIR,
user='root', group='root',
mode=0o0440)
def post_write(self):
subprocess.check_call(['update-initramfs', '-u'])
class SysCtlHardeningContext(object):
def __call__(self):
settings = utils.get_settings('os')
ctxt = {'sysctl': {}}
log("Applying sysctl settings", level=INFO)
extras = {'net_ipv4_ip_forward': 0,
'net_ipv6_conf_all_forwarding': 0,
'net_ipv6_conf_all_disable_ipv6': 1,
'net_ipv4_tcp_timestamps': 0,
'net_ipv4_conf_all_arp_ignore': 0,
'net_ipv4_conf_all_arp_announce': 0,
'kernel_sysrq': 0,
'fs_suid_dumpable': 0,
'kernel_modules_disabled': 1}
if settings['sysctl']['ipv6_enable']:
extras['net_ipv6_conf_all_disable_ipv6'] = 0
if settings['sysctl']['forwarding']:
extras['net_ipv4_ip_forward'] = 1
extras['net_ipv6_conf_all_forwarding'] = 1
if settings['sysctl']['arp_restricted']:
extras['net_ipv4_conf_all_arp_ignore'] = 1
extras['net_ipv4_conf_all_arp_announce'] = 2
if settings['security']['kernel_enable_module_loading']:
extras['kernel_modules_disabled'] = 0
if settings['sysctl']['kernel_enable_sysrq']:
sysrq_val = settings['sysctl']['kernel_secure_sysrq']
extras['kernel_sysrq'] = sysrq_val
if settings['security']['kernel_enable_core_dump']:
extras['fs_suid_dumpable'] = 1
settings.update(extras)
for d in (SYSCTL_DEFAULTS % settings).split():
d = d.strip().partition('=')
key = d[0].strip()
path = os.path.join('/proc/sys', key.replace('.', '/'))
if not os.path.exists(path):
log("Skipping '%s' since '%s' does not exist" % (key, path),
level=WARNING)
continue
ctxt['sysctl'][key] = d[2] or None
return {
'sysctl_settings': [(k, v) for k, v in ctxt['sysctl'].items()]
}
class SysctlConf(TemplatedFile):
"""An audit check for sysctl settings."""
def __init__(self):
self.conffile = '/etc/sysctl.d/99-juju-hardening.conf'
super(SysctlConf, self).__init__(self.conffile,
SysCtlHardeningContext(),
template_dir=TEMPLATES_DIR,
user='root', group='root',
mode=0o0440)
def post_write(self):
try:
subprocess.check_call(['sysctl', '-p', self.conffile])
except subprocess.CalledProcessError as e:
# NOTE: on some systems if sysctl cannot apply all settings it
# will return non-zero as well.
log("sysctl command returned an error (maybe some "
"keys could not be set) - %s" % (e),
level=WARNING)

View File

@@ -1,8 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
{% if disable_core_dump -%}
# Prevent core dumps for all users. These are usually only needed by developers and may contain sensitive information.
* hard core 0
{% endif %}

View File

@@ -1,5 +0,0 @@
TMOUT={{ tmout }}
readonly TMOUT
export TMOUT
readonly HISTFILE

View File

@@ -1,7 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
{% for key, value in sysctl_settings -%}
{{ key }}={{ value }}
{% endfor -%}

View File

@@ -1,349 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
#
# /etc/login.defs - Configuration control definitions for the login package.
#
# Three items must be defined: MAIL_DIR, ENV_SUPATH, and ENV_PATH.
# If unspecified, some arbitrary (and possibly incorrect) value will
# be assumed. All other items are optional - if not specified then
# the described action or option will be inhibited.
#
# Comment lines (lines beginning with "#") and blank lines are ignored.
#
# Modified for Linux. --marekm
# REQUIRED for useradd/userdel/usermod
# Directory where mailboxes reside, _or_ name of file, relative to the
# home directory. If you _do_ define MAIL_DIR and MAIL_FILE,
# MAIL_DIR takes precedence.
#
# Essentially:
# - MAIL_DIR defines the location of users mail spool files
# (for mbox use) by appending the username to MAIL_DIR as defined
# below.
# - MAIL_FILE defines the location of the users mail spool files as the
# fully-qualified filename obtained by prepending the user home
# directory before $MAIL_FILE
#
# NOTE: This is no more used for setting up users MAIL environment variable
# which is, starting from shadow 4.0.12-1 in Debian, entirely the
# job of the pam_mail PAM modules
# See default PAM configuration files provided for
# login, su, etc.
#
# This is a temporary situation: setting these variables will soon
# move to /etc/default/useradd and the variables will then be
# no more supported
MAIL_DIR /var/mail
#MAIL_FILE .mail
#
# Enable logging and display of /var/log/faillog login failure info.
# This option conflicts with the pam_tally PAM module.
#
FAILLOG_ENAB yes
#
# Enable display of unknown usernames when login failures are recorded.
#
# WARNING: Unknown usernames may become world readable.
# See #290803 and #298773 for details about how this could become a security
# concern
LOG_UNKFAIL_ENAB no
#
# Enable logging of successful logins
#
LOG_OK_LOGINS yes
#
# Enable "syslog" logging of su activity - in addition to sulog file logging.
# SYSLOG_SG_ENAB does the same for newgrp and sg.
#
SYSLOG_SU_ENAB yes
SYSLOG_SG_ENAB yes
#
# If defined, all su activity is logged to this file.
#
#SULOG_FILE /var/log/sulog
#
# If defined, file which maps tty line to TERM environment parameter.
# Each line of the file is in a format something like "vt100 tty01".
#
#TTYTYPE_FILE /etc/ttytype
#
# If defined, login failures will be logged here in a utmp format
# last, when invoked as lastb, will read /var/log/btmp, so...
#
FTMP_FILE /var/log/btmp
#
# If defined, the command name to display when running "su -". For
# example, if this is defined as "su" then a "ps" will display the
# command is "-su". If not defined, then "ps" would display the
# name of the shell actually being run, e.g. something like "-sh".
#
SU_NAME su
#
# If defined, file which inhibits all the usual chatter during the login
# sequence. If a full pathname, then hushed mode will be enabled if the
# user's name or shell are found in the file. If not a full pathname, then
# hushed mode will be enabled if the file exists in the user's home directory.
#
HUSHLOGIN_FILE .hushlogin
#HUSHLOGIN_FILE /etc/hushlogins
#
# *REQUIRED* The default PATH settings, for superuser and normal users.
#
# (they are minimal, add the rest in the shell startup files)
ENV_SUPATH PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV_PATH PATH=/usr/local/bin:/usr/bin:/bin{% if additional_user_paths %}{{ additional_user_paths }}{% endif %}
#
# Terminal permissions
#
# TTYGROUP Login tty will be assigned this group ownership.
# TTYPERM Login tty will be set to this permission.
#
# If you have a "write" program which is "setgid" to a special group
# which owns the terminals, define TTYGROUP to the group number and
# TTYPERM to 0620. Otherwise leave TTYGROUP commented out and assign
# TTYPERM to either 622 or 600.
#
# In Debian /usr/bin/bsd-write or similar programs are setgid tty
# However, the default and recommended value for TTYPERM is still 0600
# to not allow anyone to write to anyone else console or terminal
# Users can still allow other people to write them by issuing
# the "mesg y" command.
TTYGROUP tty
TTYPERM 0600
#
# Login configuration initializations:
#
# ERASECHAR Terminal ERASE character ('\010' = backspace).
# KILLCHAR Terminal KILL character ('\025' = CTRL/U).
# UMASK Default "umask" value.
#
# The ERASECHAR and KILLCHAR are used only on System V machines.
#
# UMASK is the default umask value for pam_umask and is used by
# useradd and newusers to set the mode of the new home directories.
# 022 is the "historical" value in Debian for UMASK
# 027, or even 077, could be considered better for privacy
# There is no One True Answer here : each sysadmin must make up his/her
# mind.
#
# If USERGROUPS_ENAB is set to "yes", that will modify this UMASK default value
# for private user groups, i. e. the uid is the same as gid, and username is
# the same as the primary group name: for these, the user permissions will be
# used as group permissions, e. g. 022 will become 002.
#
# Prefix these values with "0" to get octal, "0x" to get hexadecimal.
#
ERASECHAR 0177
KILLCHAR 025
UMASK {{ umask }}
# Enable setting of the umask group bits to be the same as owner bits (examples: `022` -> `002`, `077` -> `007`) for non-root users, if the uid is the same as gid, and username is the same as the primary group name.
# If set to yes, userdel will remove the user´s group if it contains no more members, and useradd will create by default a group with the name of the user.
USERGROUPS_ENAB yes
#
# Password aging controls:
#
# PASS_MAX_DAYS Maximum number of days a password may be used.
# PASS_MIN_DAYS Minimum number of days allowed between password changes.
# PASS_WARN_AGE Number of days warning given before a password expires.
#
PASS_MAX_DAYS {{ pwd_max_age }}
PASS_MIN_DAYS {{ pwd_min_age }}
PASS_WARN_AGE 7
#
# Min/max values for automatic uid selection in useradd
#
UID_MIN {{ uid_min }}
UID_MAX 60000
# System accounts
SYS_UID_MIN {{ sys_uid_min }}
SYS_UID_MAX {{ sys_uid_max }}
# Min/max values for automatic gid selection in groupadd
GID_MIN {{ gid_min }}
GID_MAX 60000
# System accounts
SYS_GID_MIN {{ sys_gid_min }}
SYS_GID_MAX {{ sys_gid_max }}
#
# Max number of login retries if password is bad. This will most likely be
# overridden by PAM, since the default pam_unix module has it's own built
# in of 3 retries. However, this is a safe fallback in case you are using
# an authentication module that does not enforce PAM_MAXTRIES.
#
LOGIN_RETRIES {{ login_retries }}
#
# Max time in seconds for login
#
LOGIN_TIMEOUT {{ login_timeout }}
#
# Which fields may be changed by regular users using chfn - use
# any combination of letters "frwh" (full name, room number, work
# phone, home phone). If not defined, no changes are allowed.
# For backward compatibility, "yes" = "rwh" and "no" = "frwh".
#
{% if chfn_restrict %}
CHFN_RESTRICT {{ chfn_restrict }}
{% endif %}
#
# Should login be allowed if we can't cd to the home directory?
# Default in no.
#
DEFAULT_HOME {% if allow_login_without_home %} yes {% else %} no {% endif %}
#
# If defined, this command is run when removing a user.
# It should remove any at/cron/print jobs etc. owned by
# the user to be removed (passed as the first argument).
#
#USERDEL_CMD /usr/sbin/userdel_local
#
# Enable setting of the umask group bits to be the same as owner bits
# (examples: 022 -> 002, 077 -> 007) for non-root users, if the uid is
# the same as gid, and username is the same as the primary group name.
#
# If set to yes, userdel will remove the user´s group if it contains no
# more members, and useradd will create by default a group with the name
# of the user.
#
USERGROUPS_ENAB yes
#
# Instead of the real user shell, the program specified by this parameter
# will be launched, although its visible name (argv[0]) will be the shell's.
# The program may do whatever it wants (logging, additional authentication,
# banner, ...) before running the actual shell.
#
# FAKE_SHELL /bin/fakeshell
#
# If defined, either full pathname of a file containing device names or
# a ":" delimited list of device names. Root logins will be allowed only
# upon these devices.
#
# This variable is used by login and su.
#
#CONSOLE /etc/consoles
#CONSOLE console:tty01:tty02:tty03:tty04
#
# List of groups to add to the user's supplementary group set
# when logging in on the console (as determined by the CONSOLE
# setting). Default is none.
#
# Use with caution - it is possible for users to gain permanent
# access to these groups, even when not logged in on the console.
# How to do it is left as an exercise for the reader...
#
# This variable is used by login and su.
#
#CONSOLE_GROUPS floppy:audio:cdrom
#
# If set to "yes", new passwords will be encrypted using the MD5-based
# algorithm compatible with the one used by recent releases of FreeBSD.
# It supports passwords of unlimited length and longer salt strings.
# Set to "no" if you need to copy encrypted passwords to other systems
# which don't understand the new algorithm. Default is "no".
#
# This variable is deprecated. You should use ENCRYPT_METHOD.
#
MD5_CRYPT_ENAB no
#
# If set to MD5 , MD5-based algorithm will be used for encrypting password
# If set to SHA256, SHA256-based algorithm will be used for encrypting password
# If set to SHA512, SHA512-based algorithm will be used for encrypting password
# If set to DES, DES-based algorithm will be used for encrypting password (default)
# Overrides the MD5_CRYPT_ENAB option
#
# Note: It is recommended to use a value consistent with
# the PAM modules configuration.
#
ENCRYPT_METHOD SHA512
#
# Only used if ENCRYPT_METHOD is set to SHA256 or SHA512.
#
# Define the number of SHA rounds.
# With a lot of rounds, it is more difficult to brute forcing the password.
# But note also that it more CPU resources will be needed to authenticate
# users.
#
# If not specified, the libc will choose the default number of rounds (5000).
# The values must be inside the 1000-999999999 range.
# If only one of the MIN or MAX values is set, then this value will be used.
# If MIN > MAX, the highest value will be used.
#
# SHA_CRYPT_MIN_ROUNDS 5000
# SHA_CRYPT_MAX_ROUNDS 5000
################# OBSOLETED BY PAM ##############
# #
# These options are now handled by PAM. Please #
# edit the appropriate file in /etc/pam.d/ to #
# enable the equivelants of them.
#
###############
#MOTD_FILE
#DIALUPS_CHECK_ENAB
#LASTLOG_ENAB
#MAIL_CHECK_ENAB
#OBSCURE_CHECKS_ENAB
#PORTTIME_CHECKS_ENAB
#SU_WHEEL_ONLY
#CRACKLIB_DICTPATH
#PASS_CHANGE_TRIES
#PASS_ALWAYS_WARN
#ENVIRON_FILE
#NOLOGINS_FILE
#ISSUE_FILE
#PASS_MIN_LEN
#PASS_MAX_LEN
#ULIMIT
#ENV_HZ
#CHFN_AUTH
#CHSH_AUTH
#FAIL_DELAY
################# OBSOLETED #######################
# #
# These options are no more handled by shadow. #
# #
# Shadow utilities will display a warning if they #
# still appear. #
# #
###################################################
# CLOSE_SESSIONS
# LOGIN_STRING
# NO_PASSWORD_CONSOLE
# QMAIL_DIR

View File

@@ -1,117 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.
# Parameters can be specified after the module name.
# Arch
# ----
#
# Modules for certains builds, contains support modules and some CPU-specific optimizations.
{% if arch == "x86_64" -%}
# Optimize for x86_64 cryptographic features
twofish-x86_64-3way
twofish-x86_64
aes-x86_64
salsa20-x86_64
blowfish-x86_64
{% endif -%}
{% if cpuVendor == "intel" -%}
# Intel-specific optimizations
ghash-clmulni-intel
aesni-intel
kvm-intel
{% endif -%}
{% if cpuVendor == "amd" -%}
# AMD-specific optimizations
kvm-amd
{% endif -%}
kvm
# Crypto
# ------
# Some core modules which comprise strong cryptography.
blowfish_common
blowfish_generic
ctr
cts
lrw
lzo
rmd160
rmd256
rmd320
serpent
sha512_generic
twofish_common
twofish_generic
xts
zlib
# Drivers
# -------
# Basics
lp
rtc
loop
# Filesystems
ext2
btrfs
{% if desktop_enable -%}
# Desktop
psmouse
snd
snd_ac97_codec
snd_intel8x0
snd_page_alloc
snd_pcm
snd_timer
soundcore
usbhid
{% endif -%}
# Lib
# ---
xz
# Net
# ---
# All packets needed for netfilter rules (ie iptables, ebtables).
ip_tables
x_tables
iptable_filter
iptable_nat
# Targets
ipt_LOG
ipt_REJECT
# Modules
xt_connlimit
xt_tcpudp
xt_recent
xt_limit
xt_conntrack
nf_conntrack
nf_conntrack_ipv4
nf_defrag_ipv4
xt_state
nf_nat
# Addons
xt_pknock

View File

@@ -1,11 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
Name: passwdqc password strength enforcement
Default: yes
Priority: 1024
Conflicts: cracklib
Password-Type: Primary
Password:
requisite pam_passwdqc.so {{ auth_pam_passwdqc_options }}

View File

@@ -1,8 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
# Disable core dumps via soft limits for all users. Compliance to this setting
# is voluntary and can be modified by users up to a hard limit. This setting is
# a sane default.
ulimit -S -c 0 > /dev/null 2>&1

View File

@@ -1,11 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
# A list of TTYs, from which root can log in
# see `man securetty` for reference
{% if ttys -%}
{% for tty in ttys -%}
{{ tty }}
{% endfor -%}
{% endif -%}

View File

@@ -1,14 +0,0 @@
###############################################################################
# WARNING: This configuration file is maintained by Juju. Local changes may
# be overwritten.
###############################################################################
Name: tally2 lockout after failed attempts enforcement
Default: yes
Priority: 1024
Conflicts: cracklib
Auth-Type: Primary
Auth-Initial:
required pam_tally2.so deny={{ auth_retries }} onerr=fail unlock_time={{ auth_lockout_time }}
Account-Type: Primary
Account-Initial:
required pam_tally2.so

View File

@@ -1,17 +0,0 @@
# Copyright 2016 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from os import path
TEMPLATES_DIR = path.join(path.dirname(__file__), 'templates')

Some files were not shown because too many files have changed in this diff Show More