Apparmor profile for neutron-server

Add apparmor template for neutron-server
Charm helpers sync to bring in the AppArmorContext class
Create specific NeutronApiAppArmorContext
Add aa-profile-mode in config.yaml
Apply the apparmor profile as requested: disable, enforce, complain

Change-Id: I2c2843459beca349ad3ea4482bb9512b454b726d
This commit is contained in:
David Ames 2016-03-24 07:40:40 -07:00
parent 9be47125b5
commit 4796869e9f
11 changed files with 171 additions and 17 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@ bin
tags
*.sw[nop]
*.pyc
.unit-state.db
trusty/
xenial/

View File

@ -530,3 +530,9 @@ options:
description: |
The Midokura Enterprise MidoNet password credentials to access the
repository.
aa-profile-mode:
type: string
default: 'disable'
description: |
Enable apparmor profile. Valid settings: 'complain', 'enforce' or 'disable'.
AA disabled by default.

View File

@ -20,7 +20,7 @@ import os
import re
import time
from base64 import b64decode
from subprocess import check_call
from subprocess import check_call, CalledProcessError
import six
import yaml
@ -45,6 +45,7 @@ from charmhelpers.core.hookenv import (
INFO,
WARNING,
ERROR,
status_set,
)
from charmhelpers.core.sysctl import create as sysctl_create
@ -87,7 +88,7 @@ from charmhelpers.contrib.network.ip import (
is_address_in_network,
is_bridge_member,
)
from charmhelpers.contrib.openstack.utils import get_host_ip
from charmhelpers.contrib.openstack.utils import get_host_ip, os_release
from charmhelpers.core.unitdata import kv
try:
@ -1479,3 +1480,80 @@ class NetworkServiceContext(OSContextGenerator):
if self.context_complete(ctxt):
return ctxt
return {}
class AppArmorContext(OSContextGenerator):
"""Base class for apparmor contexts."""
def __init__(self):
self.ctxt = {}
self.aa_profile = None
self.aa_utils_packages = ['apparmor-utils']
def __call__(self):
"""
Validate aa-profile-mode settings is disable, enforce, or complain.
Otherwise return an empty dictionary.
:return ctxt: Dictionary of the apparmor profile mode or empty
dictionary.
"""
if config('aa-profile-mode') in ['disable', 'enforce', 'complain']:
self.ctxt = {'aa-profile-mode': config('aa-profile-mode')}
if os_release('neutron-common') >= 'mitaka':
self.ctxt.update({'python3': True})
return self.ctxt
def install_aa_utils(self):
"""
Install packages required for apparmor configuration.
"""
log("Installing apparmor utils.")
apt_install(packages=self.aa_utils_packages,
fatal=True)
def manually_disable_aa_profile(self):
"""
Manually disable an apparmor profile.
If aa-profile-mode is set to disabled (default) this is required as the
template has been written but apparmor is yet unaware of the profile
and aa-disable aa-profile fails. Without this the profile would kick
into enforce mode on the next service restart.
"""
profile_path = '/etc/apparmor.d'
disable_path = '/etc/apparmor.d/disable'
if not os.path.lexists(os.path.join(disable_path, self.aa_profile)):
os.symlink(os.path.join(profile_path, self.aa_profile),
os.path.join(disable_path, self.aa_profile))
def setup_aa_profile(self):
"""
Setup an apparmor profile.
The ctxt dictionary will contain the apparmor profile mode and
the apparmor profile name.
Makes calls out to aa-disable, aa-complain, or aa-enforce to setup
the apparmor profile.
"""
ctxt = self.__call__()
if not ctxt:
log("Not enabling apparmor Profile")
return
self.install_aa_utils()
cmd = ['aa-{}'.format(ctxt['aa-profile-mode'])]
cmd.append(ctxt['aa-profile'])
log("Setting up the apparmor profile for {} in {} mode."
"".format(ctxt['aa-profile'], ctxt['aa-profile-mode']))
try:
check_call(cmd)
except CalledProcessError as e:
if ctxt['aa-profile-mode'] == 'disable':
log("Manually disabling the apparmor profile for {}."
"".format(ctxt['aa-profile']))
self.manually_disable_aa_profile()
return
status_set('blocked', "Apparmor profile {} failed to be set to {}."
"".format(ctxt['aa-profile'],
ctxt['aa-profile-mode']))
raise e

View File

@ -1,20 +1,12 @@
{% if auth_host -%}
{% if api_version == '3' -%}
[keystone_authtoken]
auth_url = {{ service_protocol }}://{{ service_host }}:{{ service_port }}
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}
auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}
auth_plugin = password
project_domain_id = default
user_domain_id = default
project_name = {{ admin_tenant_name }}
username = {{ admin_user }}
password = {{ admin_password }}
project_domain_name = default
user_domain_name = default
auth_plugin = password
{% else -%}
[keystone_authtoken]
identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }}
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
admin_tenant_name = {{ admin_tenant_name }}
admin_user = {{ admin_user }}
admin_password = {{ admin_password }}
signing_dir = {{ signing_dir }}
{% endif -%}
{% endif -%}

View File

@ -0,0 +1,10 @@
{% if auth_host -%}
[keystone_authtoken]
# Juno specific config (Bug #1557223)
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}
admin_tenant_name = {{ admin_tenant_name }}
admin_user = {{ admin_user }}
admin_password = {{ admin_password }}
signing_dir = {{ signing_dir }}
{% endif -%}

View File

@ -14,10 +14,12 @@ from charmhelpers.contrib.openstack.utils import (
os_release,
)
VLAN = 'vlan'
VXLAN = 'vxlan'
GRE = 'gre'
OVERLAY_NET_TYPES = [VXLAN, GRE]
NEUTRON_API_AA_PROFILE = 'usr.bin.neutron-server'
def get_l2population():
@ -379,3 +381,17 @@ class MidonetContext(context.OSContextGenerator):
if self.context_complete(ctxt):
return ctxt
return {}
class NeutronAPIAppArmorContext(context.AppArmorContext):
def __init__(self):
super(NeutronAPIAppArmorContext, self).__init__()
self.aa_profile = NEUTRON_API_AA_PROFILE
def __call__(self):
self.ctxt = super(NeutronAPIAppArmorContext, self).__call__()
if not self.ctxt:
return self.ctxt
self.ctxt.update({'aa-profile': self.aa_profile})
return self.ctxt

View File

@ -80,6 +80,7 @@ from neutron_api_context import (
get_overlay_network_type,
IdentityServiceContext,
EtcdContext,
NeutronAPIAppArmorContext,
)
from charmhelpers.contrib.hahelpers.cluster import (
@ -289,6 +290,7 @@ def config_changed():
fatal=True)
configure_https()
update_nrpe_config()
NeutronAPIAppArmorContext().setup_aa_profile()
CONFIGS.write_all()
for r_id in relation_ids('neutron-api'):
neutron_api_relation_joined(rid=r_id)

View File

@ -135,6 +135,8 @@ APACHE_CONF = '/etc/apache2/sites-available/openstack_https_frontend'
APACHE_24_CONF = '/etc/apache2/sites-available/openstack_https_frontend.conf'
NEUTRON_DEFAULT = '/etc/default/neutron-server'
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
NEUTRON_API_AA_PROFILE = ('/etc/apparmor.d/{}'
''.format(neutron_api_context.NEUTRON_API_AA_PROFILE))
BASE_RESOURCE_MAP = OrderedDict([
(NEUTRON_CONF, {
@ -172,6 +174,10 @@ BASE_RESOURCE_MAP = OrderedDict([
neutron_api_context.HAProxyContext()],
'services': ['haproxy'],
}),
(NEUTRON_API_AA_PROFILE, {
'services': ['neutron-server'],
'contexts': [neutron_api_context.NeutronAPIAppArmorContext()],
}),
])
# The interface is said to be satisfied if anyone of the interfaces in the

View File

@ -0,0 +1,32 @@
# Last Modified: Tue Mar 22 22:08:21 2016
#include <tunables/global>
/usr/bin/neutron-server {%if aa_profile_mode == 'complain' %}flags=(comlain){% endif %} {
#include <abstractions/apache2-common>
#include <abstractions/base>
#include <abstractions/python>
/* a,
/bin/dash rix,
/bin/uname rix,
/etc/mime.types r,
/etc/neutron/* r,
/etc/neutron/plugins/ml2/* r,
/etc/neutron/policy.d/ r,
/proc/*/mounts r,
/proc/*/status r,
/sbin/ldconfig rix,
/sbin/ldconfig.real rix,
/tmp/* rw,
/usr/bin/ r,
/usr/bin/neutron-server r,
{%if python3 %}
/usr/bin/python3.5 ix,
{% else %}
/usr/bin/python2.7 ix,
{% endif %}
/var/lib/neutron/* a,
/var/log/neutron/neutron-server.log w,
/var/tmp/* a,
}

View File

@ -782,15 +782,20 @@ class AmuletUtils(object):
# amulet juju action helpers:
def run_action(self, unit_sentry, action,
_check_output=subprocess.check_output):
_check_output=subprocess.check_output,
params=None):
"""Run the named action on a given unit sentry.
params a dict of parameters to use
_check_output parameter is used for dependency injection.
@return action_id.
"""
unit_id = unit_sentry.info["unit_name"]
command = ["juju", "action", "do", "--format=json", unit_id, action]
if params is not None:
for key, value in params.iteritems():
command.append("{}={}".format(key, value))
self.log.info("Running command: %s\n" % " ".join(command))
output = _check_output(command, universal_newlines=True)
data = json.loads(output)

View File

@ -216,6 +216,9 @@ class TestNeutronAPIUtils(CharmTestCase):
(nutils.HAPROXY_CONF, {
'services': ['haproxy'],
}),
(nutils.NEUTRON_API_AA_PROFILE, {
'services': ['neutron-server'],
}),
])
self.assertItemsEqual(_restart_map, expect)
@ -238,7 +241,8 @@ class TestNeutronAPIUtils(CharmTestCase):
'/etc/default/neutron-server',
'/etc/neutron/plugins/ml2/ml2_conf.ini',
'/etc/apache2/sites-available/openstack_https_frontend',
'/etc/haproxy/haproxy.cfg']
'/etc/haproxy/haproxy.cfg',
'/etc/apparmor.d/usr.bin.neutron-server']
self.assertItemsEqual(_regconfs.configs, confs)
@patch('os.path.isfile')