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:
34
Makefile
34
Makefile
@@ -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
6
TODO
@@ -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
|
|
@@ -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')
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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__':
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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__':
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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__':
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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__':
|
||||||
|
@@ -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
|
|
@@ -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:
|
||||||
|
@@ -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.
|
|
@@ -1 +0,0 @@
|
|||||||
ceph_hooks.py
|
|
@@ -1 +0,0 @@
|
|||||||
ceph_hooks.py
|
|
@@ -1 +0,0 @@
|
|||||||
ceph_hooks.py
|
|
@@ -1 +0,0 @@
|
|||||||
ceph_hooks.py
|
|
@@ -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
|
|
@@ -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': '*'}
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -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)
|
|
@@ -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']
|
|
@@ -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.
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -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')
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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 }}
|
|
@@ -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>
|
|
@@ -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
|
|
@@ -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()
|
|
@@ -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))
|
|
@@ -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)
|
|
@@ -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'
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -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)
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -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')
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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'))
|
|
@@ -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)
|
|
@@ -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 %}
|
|
@@ -1,5 +0,0 @@
|
|||||||
TMOUT={{ tmout }}
|
|
||||||
readonly TMOUT
|
|
||||||
export TMOUT
|
|
||||||
|
|
||||||
readonly HISTFILE
|
|
@@ -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 -%}
|
|
@@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -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
|
|
@@ -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 }}
|
|
@@ -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
|
|
@@ -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 -%}
|
|
@@ -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
|
|
@@ -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
Reference in New Issue
Block a user