Updates to enable jammy and finalise charmcraft builds
- Add 22.04 to charmcraft.yaml - Update metadata to include jammy - Remove impish from metadata - Update osci.yaml to include py3.10 default job - Modify tox.ini to remove py35,py36,py37 tox target and add py310 target. - ensure that the openstack-origin is yoga - charmhelpers sync func-test-pr: https://github.com/openstack-charmers/zaza-openstack-tests/pull/747 Change-Id: I8bd5e6f5d002fbd1118f2c7fa54e50f1b31fc722
This commit is contained in:
parent
d373c46450
commit
08c823a9b4
@ -1,5 +1,4 @@
|
||||
- project:
|
||||
templates:
|
||||
- python35-charm-jobs
|
||||
- openstack-python3-ussuri-jobs
|
||||
- openstack-python3-charm-yoga-jobs
|
||||
- openstack-cover-jobs
|
||||
|
@ -6,6 +6,7 @@ include:
|
||||
- cli
|
||||
- fetch
|
||||
- contrib.openstack|inc=*
|
||||
- contrib.hardware.pci
|
||||
- contrib.hahelpers
|
||||
- contrib.network
|
||||
- contrib.python
|
||||
|
@ -21,7 +21,15 @@ parts:
|
||||
- README.md
|
||||
|
||||
bases:
|
||||
- name: ubuntu
|
||||
channel: "20.04"
|
||||
architectures:
|
||||
- amd64
|
||||
- build-on:
|
||||
- name: ubuntu
|
||||
channel: "20.04"
|
||||
architectures:
|
||||
- amd64
|
||||
run-on:
|
||||
- name: ubuntu
|
||||
channel: "20.04"
|
||||
architectures: [amd64, s390x, ppc64el, arm64]
|
||||
- name: ubuntu
|
||||
channel: "22.04"
|
||||
architectures: [amd64, s390x, ppc64el, arm64]
|
||||
|
@ -14,7 +14,7 @@ options:
|
||||
Setting this to True will allow supporting services to log to syslog.
|
||||
openstack-origin:
|
||||
type: string
|
||||
default: distro
|
||||
default: yoga
|
||||
description: |
|
||||
Repository from which to install. May be one of the following:
|
||||
distro (default), ppa:somecustom/ppa, a deb url sources entry,
|
||||
|
@ -14,30 +14,15 @@
|
||||
|
||||
# Bootstrap charm-helpers, installing its dependencies if necessary using
|
||||
# only standard libraries.
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
import six # NOQA:F401
|
||||
except ImportError:
|
||||
if sys.version_info.major == 2:
|
||||
subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
|
||||
else:
|
||||
subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
|
||||
import six # NOQA:F401
|
||||
|
||||
try:
|
||||
import yaml # NOQA:F401
|
||||
except ImportError:
|
||||
if sys.version_info.major == 2:
|
||||
subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
|
||||
else:
|
||||
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
|
||||
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
|
||||
import yaml # NOQA:F401
|
||||
|
||||
|
||||
|
@ -16,9 +16,6 @@ import inspect
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import six
|
||||
from six.moves import zip
|
||||
|
||||
import charmhelpers.core.unitdata
|
||||
|
||||
|
||||
@ -149,10 +146,7 @@ class CommandLine(object):
|
||||
def run(self):
|
||||
"Run cli, processing arguments and executing subcommands."
|
||||
arguments = self.argument_parser.parse_args()
|
||||
if six.PY2:
|
||||
argspec = inspect.getargspec(arguments.func)
|
||||
else:
|
||||
argspec = inspect.getfullargspec(arguments.func)
|
||||
argspec = inspect.getfullargspec(arguments.func)
|
||||
vargs = []
|
||||
for arg in argspec.args:
|
||||
vargs.append(getattr(arguments, arg))
|
||||
@ -177,10 +171,7 @@ 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."""
|
||||
|
||||
if six.PY2:
|
||||
argspec = inspect.getargspec(func)
|
||||
else:
|
||||
argspec = inspect.getfullargspec(func)
|
||||
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)]
|
||||
|
@ -28,6 +28,7 @@ import subprocess
|
||||
import yaml
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
application_name,
|
||||
config,
|
||||
hook_name,
|
||||
local_unit,
|
||||
@ -174,7 +175,8 @@ define service {{
|
||||
if os.path.exists(os.path.join(path, parts[0])):
|
||||
command = os.path.join(path, parts[0])
|
||||
if len(parts) > 1:
|
||||
command += " " + " ".join(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 ''
|
||||
@ -520,3 +522,39 @@ def remove_deprecated_check(nrpe, deprecated_services):
|
||||
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)
|
||||
|
@ -32,8 +32,6 @@ import time
|
||||
|
||||
from socket import gethostname as get_unit_hostname
|
||||
|
||||
import six
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
log,
|
||||
relation_ids,
|
||||
@ -125,16 +123,16 @@ def is_crm_dc():
|
||||
"""
|
||||
cmd = ['crm', 'status']
|
||||
try:
|
||||
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||
if not isinstance(status, six.text_type):
|
||||
status = six.text_type(status, "utf-8")
|
||||
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: juju-lytrusty-machine-2 (168108163)
|
||||
# - partition with quorum
|
||||
current_dc = line.split(':')[1].split()[0]
|
||||
if current_dc == get_unit_hostname():
|
||||
return True
|
||||
@ -158,9 +156,8 @@ def is_crm_leader(resource, retry=False):
|
||||
return is_crm_dc()
|
||||
cmd = ['crm', 'resource', 'show', resource]
|
||||
try:
|
||||
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||
if not isinstance(status, six.text_type):
|
||||
status = six.text_type(status, "utf-8")
|
||||
status = subprocess.check_output(
|
||||
cmd, stderr=subprocess.STDOUT).decode('utf-8')
|
||||
except subprocess.CalledProcessError:
|
||||
status = None
|
||||
|
||||
|
@ -14,7 +14,6 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import subprocess
|
||||
|
||||
|
||||
@ -95,9 +94,7 @@ class ApacheConfContext(object):
|
||||
settings = utils.get_settings('apache')
|
||||
ctxt = settings['hardening']
|
||||
|
||||
out = subprocess.check_output(['apache2', '-v'])
|
||||
if six.PY3:
|
||||
out = out.decode('utf-8')
|
||||
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/'
|
||||
|
@ -15,8 +15,6 @@
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import six
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
log,
|
||||
INFO,
|
||||
@ -35,7 +33,7 @@ class DisabledModuleAudit(BaseAudit):
|
||||
def __init__(self, modules):
|
||||
if modules is None:
|
||||
self.modules = []
|
||||
elif isinstance(modules, six.string_types):
|
||||
elif isinstance(modules, str):
|
||||
self.modules = [modules]
|
||||
else:
|
||||
self.modules = modules
|
||||
@ -68,9 +66,7 @@ class DisabledModuleAudit(BaseAudit):
|
||||
@staticmethod
|
||||
def _get_loaded_modules():
|
||||
"""Returns the modules which are enabled in Apache."""
|
||||
output = subprocess.check_output(['apache2ctl', '-M'])
|
||||
if six.PY3:
|
||||
output = output.decode('utf-8')
|
||||
output = subprocess.check_output(['apache2ctl', '-M']).decode('utf-8')
|
||||
modules = []
|
||||
for line in output.splitlines():
|
||||
# Each line of the enabled module output looks like:
|
||||
|
@ -12,9 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import # required for external apt import
|
||||
from six import string_types
|
||||
|
||||
from charmhelpers.fetch import (
|
||||
apt_cache,
|
||||
apt_purge
|
||||
@ -51,7 +48,7 @@ class RestrictedPackages(BaseAudit):
|
||||
|
||||
def __init__(self, pkgs, **kwargs):
|
||||
super(RestrictedPackages, self).__init__(**kwargs)
|
||||
if isinstance(pkgs, string_types) or not hasattr(pkgs, '__iter__'):
|
||||
if isinstance(pkgs, str) or not hasattr(pkgs, '__iter__'):
|
||||
self.pkgs = pkgs.split()
|
||||
else:
|
||||
self.pkgs = pkgs
|
||||
|
@ -23,7 +23,6 @@ from subprocess import (
|
||||
check_call,
|
||||
)
|
||||
from traceback import format_exc
|
||||
from six import string_types
|
||||
from stat import (
|
||||
S_ISGID,
|
||||
S_ISUID
|
||||
@ -63,7 +62,7 @@ class BaseFileAudit(BaseAudit):
|
||||
"""
|
||||
super(BaseFileAudit, self).__init__(*args, **kwargs)
|
||||
self.always_comply = always_comply
|
||||
if isinstance(paths, string_types) or not hasattr(paths, '__iter__'):
|
||||
if isinstance(paths, str) or not hasattr(paths, '__iter__'):
|
||||
self.paths = [paths]
|
||||
else:
|
||||
self.paths = paths
|
||||
|
@ -12,8 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import six
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
@ -53,18 +51,17 @@ def harden(overrides=None):
|
||||
overrides = []
|
||||
|
||||
def _harden_inner1(f):
|
||||
# As this has to be py2.7 compat, we can't use nonlocal. Use a trick
|
||||
# to capture the dictionary that can then be updated.
|
||||
_logged = {'done': False}
|
||||
_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['done']:
|
||||
if not _logged:
|
||||
log("Hardening function '%s'" % (f.__name__), level=DEBUG)
|
||||
_logged['done'] = True
|
||||
_logged = True
|
||||
RUN_CATALOG = OrderedDict([('os', run_os_checks),
|
||||
('ssh', run_ssh_checks),
|
||||
('mysql', run_mysql_checks),
|
||||
@ -74,7 +71,7 @@ def harden(overrides=None):
|
||||
if enabled:
|
||||
modules_to_run = []
|
||||
# modules will always be performed in the following order
|
||||
for module, func in six.iteritems(RUN_CATALOG):
|
||||
for module, func in RUN_CATALOG.items():
|
||||
if module in enabled:
|
||||
enabled.remove(module)
|
||||
modules_to_run.append(func)
|
||||
|
@ -12,8 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from six import string_types
|
||||
|
||||
from charmhelpers.contrib.hardening.audits.file import TemplatedFile
|
||||
from charmhelpers.contrib.hardening.host import TEMPLATES_DIR
|
||||
from charmhelpers.contrib.hardening import utils
|
||||
@ -41,7 +39,7 @@ class LoginContext(object):
|
||||
# a string assume it to be octal and turn it into an octal
|
||||
# string.
|
||||
umask = settings['environment']['umask']
|
||||
if not isinstance(umask, string_types):
|
||||
if not isinstance(umask, str):
|
||||
umask = '%s' % oct(umask)
|
||||
|
||||
ctxt = {
|
||||
|
@ -15,7 +15,6 @@
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import six
|
||||
import subprocess
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
@ -183,9 +182,9 @@ class SysCtlHardeningContext(object):
|
||||
|
||||
ctxt['sysctl'][key] = d[2] or None
|
||||
|
||||
# Translate for python3
|
||||
return {'sysctl_settings':
|
||||
[(k, v) for k, v in six.iteritems(ctxt['sysctl'])]}
|
||||
return {
|
||||
'sysctl_settings': [(k, v) for k, v in ctxt['sysctl'].items()]
|
||||
}
|
||||
|
||||
|
||||
class SysctlConf(TemplatedFile):
|
||||
|
@ -12,7 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import six
|
||||
import subprocess
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
@ -82,6 +81,6 @@ class MySQLConfContext(object):
|
||||
"""
|
||||
def __call__(self):
|
||||
settings = utils.get_settings('mysql')
|
||||
# Translate for python3
|
||||
return {'mysql_settings':
|
||||
[(k, v) for k, v in six.iteritems(settings['security'])]}
|
||||
return {
|
||||
'mysql_settings': [(k, v) for k, v in settings['security'].items()]
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import six
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
log,
|
||||
@ -27,10 +26,7 @@ except ImportError:
|
||||
from charmhelpers.fetch import apt_install
|
||||
from charmhelpers.fetch import apt_update
|
||||
apt_update(fatal=True)
|
||||
if six.PY2:
|
||||
apt_install('python-jinja2', fatal=True)
|
||||
else:
|
||||
apt_install('python3-jinja2', fatal=True)
|
||||
apt_install('python3-jinja2', fatal=True)
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
|
||||
|
||||
|
@ -16,7 +16,6 @@ import glob
|
||||
import grp
|
||||
import os
|
||||
import pwd
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
@ -91,7 +90,7 @@ def _apply_overrides(settings, overrides, schema):
|
||||
:returns: dictionary of modules config with user overrides applied.
|
||||
"""
|
||||
if overrides:
|
||||
for k, v in six.iteritems(overrides):
|
||||
for k, v in overrides.items():
|
||||
if k in schema:
|
||||
if schema[k] is None:
|
||||
settings[k] = v
|
||||
|
13
hooks/charmhelpers/contrib/hardware/__init__.py
Normal file
13
hooks/charmhelpers/contrib/hardware/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright 2022 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.
|
288
hooks/charmhelpers/contrib/hardware/pci.py
Normal file
288
hooks/charmhelpers/contrib/hardware/pci.py
Normal file
@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2016-2022 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.
|
||||
|
||||
import glob
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import typing
|
||||
|
||||
|
||||
def format_pci_addr(pci_addr: str) -> str:
|
||||
"""Format a PCI address with 0 fill for parts
|
||||
|
||||
:param: pci_addr: unformatted PCI address
|
||||
:type: str
|
||||
:returns: formatted PCI address
|
||||
:rtype: str
|
||||
"""
|
||||
domain, bus, slot_func = pci_addr.split(":")
|
||||
slot, func = slot_func.split(".")
|
||||
return "{}:{}:{}.{}".format(
|
||||
domain.zfill(4), bus.zfill(2), slot.zfill(2), func
|
||||
)
|
||||
|
||||
|
||||
def get_sysnet_interfaces_and_macs() -> list:
|
||||
"""Catalog interface information from local system
|
||||
|
||||
each device dict contains:
|
||||
|
||||
interface: logical name
|
||||
mac_address: MAC address
|
||||
pci_address: PCI address
|
||||
state: Current interface state (up/down)
|
||||
sriov: Boolean indicating whether interface is an SR-IOV
|
||||
capable device.
|
||||
sriov_totalvfs: Total VF capacity of device
|
||||
sriov_numvfs: Configured VF capacity of device
|
||||
|
||||
:returns: array of dict objects containing details of each interface
|
||||
:rtype: list
|
||||
"""
|
||||
net_devs = []
|
||||
for sdir in itertools.chain(
|
||||
glob.glob("/sys/bus/pci/devices/*/net/../"),
|
||||
glob.glob("/sys/bus/pci/devices/*/virtio*/net/../")):
|
||||
fq_path = os.path.realpath(sdir)
|
||||
path = fq_path.split("/")
|
||||
if "virtio" in path[-1]:
|
||||
pci_address = path[-2]
|
||||
else:
|
||||
pci_address = path[-1]
|
||||
ifname = get_sysnet_interface(sdir)
|
||||
if not ifname:
|
||||
logging.warn("Unable to determine interface name for PCI "
|
||||
"device {}".format(pci_address))
|
||||
continue
|
||||
device = {
|
||||
"interface": ifname,
|
||||
"mac_address": get_sysnet_mac(sdir, ifname),
|
||||
"pci_address": pci_address,
|
||||
"state": get_sysnet_device_state(sdir, ifname),
|
||||
"sriov": is_sriov(sdir),
|
||||
}
|
||||
if device["sriov"]:
|
||||
device["sriov_totalvfs"] = get_sriov_totalvfs(sdir)
|
||||
device["sriov_numvfs"] = get_sriov_numvfs(sdir)
|
||||
net_devs.append(device)
|
||||
|
||||
return net_devs
|
||||
|
||||
|
||||
def get_sysnet_mac(sysdir: str, ifname: str) -> str:
|
||||
"""Determine MAC address for a device
|
||||
|
||||
:param: sysdir: path to device /sys directory
|
||||
:type: str
|
||||
:returns: MAC address of device
|
||||
:rtype: str
|
||||
"""
|
||||
mac_addr_file = os.path.join(sysdir, "net", ifname, "address")
|
||||
with open(mac_addr_file, "r") as f:
|
||||
read_data = f.read()
|
||||
return read_data.strip()
|
||||
|
||||
|
||||
def get_sysnet_device_state(sysdir: str, ifname: str) -> str:
|
||||
"""Read operational state of a device
|
||||
|
||||
:param: sysdir: path to device /sys directory
|
||||
:type: str
|
||||
:returns: current device state
|
||||
:rtype: str
|
||||
"""
|
||||
state_file = os.path.join(sysdir, "net", ifname, "operstate")
|
||||
with open(state_file, "r") as f:
|
||||
read_data = f.read()
|
||||
return read_data.strip()
|
||||
|
||||
|
||||
def is_sriov(sysdir: str) -> bool:
|
||||
"""Determine whether a device is SR-IOV capable
|
||||
|
||||
:param: sysdir: path to device /sys directory
|
||||
:type: str
|
||||
:returns: whether device is SR-IOV capable or not
|
||||
:rtype: bool
|
||||
"""
|
||||
return os.path.exists(os.path.join(sysdir, "sriov_totalvfs"))
|
||||
|
||||
|
||||
def get_sriov_totalvfs(sysdir: str) -> int:
|
||||
"""Read total VF capacity for a device
|
||||
|
||||
:param: sysdir: path to device /sys directory
|
||||
:type: str
|
||||
:returns: number of VF's the device supports
|
||||
:rtype: int
|
||||
"""
|
||||
sriov_totalvfs_file = os.path.join(sysdir, "sriov_totalvfs")
|
||||
with open(sriov_totalvfs_file, "r") as f:
|
||||
read_data = f.read()
|
||||
return int(read_data.strip())
|
||||
|
||||
|
||||
def get_sriov_numvfs(sysdir: str) -> int:
|
||||
"""Read configured VF capacity for a device
|
||||
|
||||
:param: sysdir: path to device /sys directory
|
||||
:type: str
|
||||
:returns: number of VF's the device is configured with
|
||||
:rtype: int
|
||||
"""
|
||||
sriov_numvfs_file = os.path.join(sysdir, "sriov_numvfs")
|
||||
with open(sriov_numvfs_file, "r") as f:
|
||||
read_data = f.read()
|
||||
return int(read_data.strip())
|
||||
|
||||
|
||||
# https://github.com/libvirt/libvirt/commit/5b1c525b1f3608156884aed0dc5e925306c1e260
|
||||
PF_PHYS_PORT_NAME_REGEX = re.compile(r"(p[0-9]+$)|(p[0-9]+s[0-9]+$)",
|
||||
re.IGNORECASE)
|
||||
|
||||
|
||||
def _phys_port_name_is_pf(sysnetdir: str) -> typing.Optional[bool]:
|
||||
try:
|
||||
with open(os.path.join(sysnetdir, "phys_port_name"), "r") as fin:
|
||||
return (PF_PHYS_PORT_NAME_REGEX.match(fin.read().strip())
|
||||
is not None)
|
||||
except OSError:
|
||||
return
|
||||
|
||||
|
||||
def get_sysnet_interface(sysdir: str) -> typing.Optional[str]:
|
||||
sysnetdir = os.path.join(sysdir, "net")
|
||||
netdevs = os.listdir(sysnetdir)
|
||||
# Return early in case the PCI device only has one netdev
|
||||
if len(netdevs) == 1:
|
||||
return netdevs[0]
|
||||
|
||||
# When a PCI device has multiple netdevs we need to figure out which one
|
||||
# represents the PF
|
||||
for netdev in netdevs:
|
||||
if _phys_port_name_is_pf(os.path.join(sysnetdir, netdev)):
|
||||
return netdev
|
||||
|
||||
|
||||
def get_pci_ethernet_addresses() -> list:
|
||||
"""Generate list of PCI addresses for all network adapters
|
||||
|
||||
:returns: list of PCI addresses
|
||||
:rtype: list
|
||||
"""
|
||||
cmd = ["lspci", "-m", "-D"]
|
||||
lspci_output = subprocess.check_output(cmd).decode("UTF-8")
|
||||
pci_addresses = []
|
||||
for line in lspci_output.split("\n"):
|
||||
columns = shlex.split(line)
|
||||
if len(columns) > 1 and columns[1] == "Ethernet controller":
|
||||
pci_address = columns[0]
|
||||
pci_addresses.append(format_pci_addr(pci_address))
|
||||
return pci_addresses
|
||||
|
||||
|
||||
class PCINetDevice(object):
|
||||
def __init__(self, pci_address):
|
||||
self.pci_address = pci_address
|
||||
self.interface_name = None
|
||||
self.mac_address = None
|
||||
self.state = None
|
||||
self.sriov = False
|
||||
self.sriov_totalvfs = None
|
||||
self.sriov_numvfs = None
|
||||
self.update_attributes()
|
||||
|
||||
def update_attributes(self):
|
||||
self.update_interface_info()
|
||||
|
||||
def update_interface_info(self):
|
||||
net_devices = get_sysnet_interfaces_and_macs()
|
||||
for interface in net_devices:
|
||||
if self.pci_address == interface["pci_address"]:
|
||||
self.interface_name = interface["interface"]
|
||||
self.mac_address = interface["mac_address"]
|
||||
self.state = interface["state"]
|
||||
self.sriov = interface["sriov"]
|
||||
if self.sriov:
|
||||
self.sriov_totalvfs = interface["sriov_totalvfs"]
|
||||
self.sriov_numvfs = interface["sriov_numvfs"]
|
||||
|
||||
def _set_sriov_numvfs(self, numvfs: int):
|
||||
sdevice = os.path.join(
|
||||
"/sys/bus/pci/devices", self.pci_address, "sriov_numvfs"
|
||||
)
|
||||
with open(sdevice, "w") as sh:
|
||||
sh.write(str(numvfs))
|
||||
self.update_attributes()
|
||||
|
||||
def set_sriov_numvfs(self, numvfs: int) -> bool:
|
||||
"""Set the number of VF devices for a SR-IOV PF
|
||||
|
||||
Assuming the device is an SR-IOV device, this function will attempt
|
||||
to change the number of VF's created by the PF.
|
||||
|
||||
@param numvfs: integer to set the current number of VF's to
|
||||
@returns boolean indicating whether any changes where made
|
||||
"""
|
||||
if self.sriov and numvfs != self.sriov_numvfs:
|
||||
# NOTE(fnordahl): run-time change of numvfs is disallowed
|
||||
# without resetting to 0 first.
|
||||
self._set_sriov_numvfs(0)
|
||||
self._set_sriov_numvfs(numvfs)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PCINetDevices(object):
|
||||
def __init__(self):
|
||||
self.pci_devices = [
|
||||
PCINetDevice(dev) for dev in get_pci_ethernet_addresses()
|
||||
]
|
||||
|
||||
def update_devices(self):
|
||||
for pcidev in self.pci_devices:
|
||||
pcidev.update_attributes()
|
||||
|
||||
def get_macs(self) -> list:
|
||||
macs = []
|
||||
for pcidev in self.pci_devices:
|
||||
if pcidev.mac_address:
|
||||
macs.append(pcidev.mac_address)
|
||||
return macs
|
||||
|
||||
def get_device_from_mac(self, mac: str) -> PCINetDevice:
|
||||
for pcidev in self.pci_devices:
|
||||
if pcidev.mac_address == mac:
|
||||
return pcidev
|
||||
return None
|
||||
|
||||
def get_device_from_pci_address(self, pci_addr: str) -> PCINetDevice:
|
||||
for pcidev in self.pci_devices:
|
||||
if pcidev.pci_address == pci_addr:
|
||||
return pcidev
|
||||
return None
|
||||
|
||||
def get_device_from_interface_name(
|
||||
self, interface_name: str
|
||||
) -> PCINetDevice:
|
||||
for pcidev in self.pci_devices:
|
||||
if pcidev.interface_name == interface_name:
|
||||
return pcidev
|
||||
return None
|
@ -15,7 +15,6 @@
|
||||
import glob
|
||||
import re
|
||||
import subprocess
|
||||
import six
|
||||
import socket
|
||||
|
||||
from functools import partial
|
||||
@ -39,20 +38,14 @@ try:
|
||||
import netifaces
|
||||
except ImportError:
|
||||
apt_update(fatal=True)
|
||||
if six.PY2:
|
||||
apt_install('python-netifaces', fatal=True)
|
||||
else:
|
||||
apt_install('python3-netifaces', fatal=True)
|
||||
apt_install('python3-netifaces', fatal=True)
|
||||
import netifaces
|
||||
|
||||
try:
|
||||
import netaddr
|
||||
except ImportError:
|
||||
apt_update(fatal=True)
|
||||
if six.PY2:
|
||||
apt_install('python-netaddr', fatal=True)
|
||||
else:
|
||||
apt_install('python3-netaddr', fatal=True)
|
||||
apt_install('python3-netaddr', fatal=True)
|
||||
import netaddr
|
||||
|
||||
|
||||
@ -462,15 +455,12 @@ def ns_query(address):
|
||||
try:
|
||||
import dns.resolver
|
||||
except ImportError:
|
||||
if six.PY2:
|
||||
apt_install('python-dnspython', fatal=True)
|
||||
else:
|
||||
apt_install('python3-dnspython', fatal=True)
|
||||
apt_install('python3-dnspython', fatal=True)
|
||||
import dns.resolver
|
||||
|
||||
if isinstance(address, dns.name.Name):
|
||||
rtype = 'PTR'
|
||||
elif isinstance(address, six.string_types):
|
||||
elif isinstance(address, str):
|
||||
rtype = 'A'
|
||||
else:
|
||||
return None
|
||||
@ -513,10 +503,7 @@ def get_hostname(address, fqdn=True):
|
||||
try:
|
||||
import dns.reversename
|
||||
except ImportError:
|
||||
if six.PY2:
|
||||
apt_install("python-dnspython", fatal=True)
|
||||
else:
|
||||
apt_install("python3-dnspython", fatal=True)
|
||||
apt_install("python3-dnspython", fatal=True)
|
||||
import dns.reversename
|
||||
|
||||
rev = dns.reversename.from_address(address)
|
||||
|
@ -17,7 +17,6 @@ import collections
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import subprocess
|
||||
|
||||
from charmhelpers import deprecate
|
||||
@ -367,10 +366,7 @@ def add_ovsbridge_linuxbridge(name, bridge, ifdata=None, portdata=None):
|
||||
try:
|
||||
import netifaces
|
||||
except ImportError:
|
||||
if six.PY2:
|
||||
apt_install('python-netifaces', fatal=True)
|
||||
else:
|
||||
apt_install('python3-netifaces', fatal=True)
|
||||
apt_install('python3-netifaces', fatal=True)
|
||||
import netifaces
|
||||
|
||||
# NOTE(jamespage):
|
||||
|
@ -126,24 +126,27 @@ class SimpleOVSDB(object):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, tool):
|
||||
def __init__(self, tool, args=None):
|
||||
"""SimpleOVSDB constructor.
|
||||
|
||||
:param tool: Which tool with database commands to operate on.
|
||||
Usually one of `ovs-vsctl`, `ovn-nbctl`, `ovn-sbctl`
|
||||
:type tool: str
|
||||
:param args: Extra arguments to pass to the tool
|
||||
:type args: Optional[List[str]]
|
||||
"""
|
||||
if tool not in self._tool_table_map:
|
||||
raise RuntimeError(
|
||||
'tool must be one of "{}"'.format(self._tool_table_map.keys()))
|
||||
self._tool = tool
|
||||
self._args = args
|
||||
|
||||
def __getattr__(self, table):
|
||||
if table not in self._tool_table_map[self._tool]:
|
||||
raise AttributeError(
|
||||
'table "{}" not known for use with "{}"'
|
||||
.format(table, self._tool))
|
||||
return self.Table(self._tool, table)
|
||||
return self.Table(self._tool, table, args=self._args)
|
||||
|
||||
class Table(object):
|
||||
"""Methods to interact with contents of OVSDB tables.
|
||||
@ -153,14 +156,17 @@ class SimpleOVSDB(object):
|
||||
JSON output.
|
||||
"""
|
||||
|
||||
def __init__(self, tool, table):
|
||||
def __init__(self, tool, table, args=None):
|
||||
"""SimpleOVSDBTable constructor.
|
||||
|
||||
:param table: Which table to operate on
|
||||
:type table: str
|
||||
:param args: Extra arguments to pass to the tool
|
||||
:type args: Optional[List[str]]
|
||||
"""
|
||||
self._tool = tool
|
||||
self._table = table
|
||||
self._args = args
|
||||
|
||||
def _deserialize_ovsdb(self, data):
|
||||
"""Deserialize OVSDB RFC7047 section 5.1 data.
|
||||
@ -215,7 +221,10 @@ class SimpleOVSDB(object):
|
||||
:returns: Dictionary with data
|
||||
:rtype: Dict[str, any]
|
||||
"""
|
||||
cmd = [self._tool, '-f', 'json', 'find', self._table]
|
||||
cmd = [self._tool]
|
||||
if self._args:
|
||||
cmd.extend(self._args)
|
||||
cmd.extend(['-f', 'json', 'find', self._table])
|
||||
if condition:
|
||||
cmd.append(condition)
|
||||
output = utils._run(*cmd)
|
||||
|
@ -30,8 +30,6 @@ from subprocess import (
|
||||
check_output,
|
||||
CalledProcessError)
|
||||
|
||||
import six
|
||||
|
||||
import charmhelpers.contrib.storage.linux.ceph as ch_ceph
|
||||
|
||||
from charmhelpers.contrib.openstack.audits.openstack_security_guide import (
|
||||
@ -120,20 +118,12 @@ from charmhelpers.contrib.openstack.utils import (
|
||||
)
|
||||
from charmhelpers.core.unitdata import kv
|
||||
|
||||
try:
|
||||
from sriov_netplan_shim import pci
|
||||
except ImportError:
|
||||
# The use of the function and contexts that require the pci module is
|
||||
# optional.
|
||||
pass
|
||||
from charmhelpers.contrib.hardware import pci
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
if six.PY2:
|
||||
apt_install('python-psutil', fatal=True)
|
||||
else:
|
||||
apt_install('python3-psutil', fatal=True)
|
||||
apt_install('python3-psutil', fatal=True)
|
||||
import psutil
|
||||
|
||||
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
|
||||
@ -150,10 +140,7 @@ def ensure_packages(packages):
|
||||
|
||||
|
||||
def context_complete(ctxt):
|
||||
_missing = []
|
||||
for k, v in six.iteritems(ctxt):
|
||||
if v is None or v == '':
|
||||
_missing.append(k)
|
||||
_missing = [k for k, v in ctxt.items() if v is None or v == '']
|
||||
|
||||
if _missing:
|
||||
log('Missing required data: %s' % ' '.join(_missing), level=INFO)
|
||||
@ -180,7 +167,7 @@ class OSContextGenerator(object):
|
||||
# Fresh start
|
||||
self.complete = False
|
||||
self.missing_data = []
|
||||
for k, v in six.iteritems(ctxt):
|
||||
for k, v in ctxt.items():
|
||||
if v is None or v == '':
|
||||
if k not in self.missing_data:
|
||||
self.missing_data.append(k)
|
||||
@ -434,6 +421,9 @@ class IdentityServiceContext(OSContextGenerator):
|
||||
('password', ctxt.get('admin_password', '')),
|
||||
('signing_dir', ctxt.get('signing_dir', '')),))
|
||||
|
||||
if ctxt.get('service_type'):
|
||||
c.update((('service_type', ctxt.get('service_type')),))
|
||||
|
||||
return c
|
||||
|
||||
def __call__(self):
|
||||
@ -476,6 +466,9 @@ class IdentityServiceContext(OSContextGenerator):
|
||||
'internal_protocol': int_protocol,
|
||||
'api_version': api_version})
|
||||
|
||||
if rdata.get('service_type'):
|
||||
ctxt['service_type'] = rdata.get('service_type')
|
||||
|
||||
if float(api_version) > 2:
|
||||
ctxt.update({
|
||||
'admin_domain_name': rdata.get('service_domain'),
|
||||
@ -547,6 +540,9 @@ class IdentityCredentialsContext(IdentityServiceContext):
|
||||
'api_version': api_version
|
||||
})
|
||||
|
||||
if rdata.get('service_type'):
|
||||
ctxt['service_type'] = rdata.get('service_type')
|
||||
|
||||
if float(api_version) > 2:
|
||||
ctxt.update({'admin_domain_name':
|
||||
rdata.get('domain')})
|
||||
@ -1111,10 +1107,14 @@ class ApacheSSLContext(OSContextGenerator):
|
||||
endpoint = resolve_address(net_type)
|
||||
addresses.append((addr, endpoint))
|
||||
|
||||
return sorted(set(addresses))
|
||||
# Log the set of addresses to have a trail log and capture if tuples
|
||||
# change over time in the same unit (LP: #1952414).
|
||||
sorted_addresses = sorted(set(addresses))
|
||||
log('get_network_addresses: {}'.format(sorted_addresses))
|
||||
return sorted_addresses
|
||||
|
||||
def __call__(self):
|
||||
if isinstance(self.external_ports, six.string_types):
|
||||
if isinstance(self.external_ports, str):
|
||||
self.external_ports = [self.external_ports]
|
||||
|
||||
if not self.external_ports or not https():
|
||||
@ -1531,9 +1531,9 @@ class SubordinateConfigContext(OSContextGenerator):
|
||||
continue
|
||||
|
||||
sub_config = sub_config[self.config_file]
|
||||
for k, v in six.iteritems(sub_config):
|
||||
for k, v in sub_config.items():
|
||||
if k == 'sections':
|
||||
for section, config_list in six.iteritems(v):
|
||||
for section, config_list in v.items():
|
||||
log("adding section '%s'" % (section),
|
||||
level=DEBUG)
|
||||
if ctxt[k].get(section):
|
||||
@ -1887,8 +1887,11 @@ class DataPortContext(NeutronPortContext):
|
||||
normalized.update({port: port for port in resolved
|
||||
if port in ports})
|
||||
if resolved:
|
||||
return {normalized[port]: bridge for port, bridge in
|
||||
six.iteritems(portmap) if port in normalized.keys()}
|
||||
return {
|
||||
normalized[port]: bridge
|
||||
for port, bridge in portmap.items()
|
||||
if port in normalized.keys()
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
@ -2291,15 +2294,10 @@ class HostInfoContext(OSContextGenerator):
|
||||
name = name or socket.gethostname()
|
||||
fqdn = ''
|
||||
|
||||
if six.PY2:
|
||||
exc = socket.error
|
||||
else:
|
||||
exc = OSError
|
||||
|
||||
try:
|
||||
addrs = socket.getaddrinfo(
|
||||
name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME)
|
||||
except exc:
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
for addr in addrs:
|
||||
@ -2416,12 +2414,12 @@ class DHCPAgentContext(OSContextGenerator):
|
||||
existing_ovs_use_veth = None
|
||||
# If there is a dhcp_agent.ini file read the current setting
|
||||
if os.path.isfile(DHCP_AGENT_INI):
|
||||
# config_ini does the right thing and returns None if the setting is
|
||||
# commented.
|
||||
# config_ini does the right thing and returns None if the setting
|
||||
# is commented.
|
||||
existing_ovs_use_veth = (
|
||||
config_ini(DHCP_AGENT_INI)["DEFAULT"].get("ovs_use_veth"))
|
||||
# Convert to Bool if necessary
|
||||
if isinstance(existing_ovs_use_veth, six.string_types):
|
||||
if isinstance(existing_ovs_use_veth, str):
|
||||
return bool_from_string(existing_ovs_use_veth)
|
||||
return existing_ovs_use_veth
|
||||
|
||||
@ -3126,7 +3124,7 @@ class SRIOVContext(OSContextGenerator):
|
||||
"""Determine number of Virtual Functions (VFs) configured for device.
|
||||
|
||||
:param device: Object describing a PCI Network interface card (NIC)/
|
||||
:type device: sriov_netplan_shim.pci.PCINetDevice
|
||||
:type device: contrib.hardware.pci.PCINetDevice
|
||||
:param sriov_numvfs: Number of VFs requested for blanket configuration.
|
||||
:type sriov_numvfs: int
|
||||
:returns: Number of VFs to configure for device
|
||||
|
128
hooks/charmhelpers/contrib/openstack/files/check_deferred_restarts.py
Executable file
128
hooks/charmhelpers/contrib/openstack/files/check_deferred_restarts.py
Executable file
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Copyright 2014-2022 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.
|
||||
|
||||
"""
|
||||
Checks for services with deferred restarts.
|
||||
|
||||
This Nagios check will parse /var/lib/policy-rd.d/
|
||||
to find any restarts that are currently deferred.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
|
||||
DEFERRED_EVENTS_DIR = '/var/lib/policy-rc.d'
|
||||
|
||||
|
||||
def get_deferred_events():
|
||||
"""Return a list of deferred events dicts from policy-rc.d files.
|
||||
|
||||
Events are read from DEFERRED_EVENTS_DIR and are of the form:
|
||||
{
|
||||
action: restart,
|
||||
policy_requestor_name: rabbitmq-server,
|
||||
policy_requestor_type: charm,
|
||||
reason: 'Pkg update',
|
||||
service: rabbitmq-server,
|
||||
time: 1614328743
|
||||
}
|
||||
|
||||
:raises OSError: Raised in case of a system error while reading a policy file
|
||||
:raises yaml.YAMLError: Raised if parsing a policy file fails
|
||||
|
||||
:returns: List of deferred event dictionaries
|
||||
:rtype: list
|
||||
"""
|
||||
deferred_events_files = glob.glob(
|
||||
'{}/*.deferred'.format(DEFERRED_EVENTS_DIR))
|
||||
|
||||
deferred_events = []
|
||||
for event_file in deferred_events_files:
|
||||
with open(event_file, 'r') as f:
|
||||
event = yaml.safe_load(f)
|
||||
deferred_events.append(event)
|
||||
|
||||
return deferred_events
|
||||
|
||||
|
||||
def get_deferred_restart_services(application=None):
|
||||
"""Returns a list of services with deferred restarts.
|
||||
|
||||
:param str application: Name of the application that blocked the service restart.
|
||||
If application is None, all services with deferred restarts
|
||||
are returned. Services which are blocked by a non-charm
|
||||
requestor are always returned.
|
||||
|
||||
:raises OSError: Raised in case of a system error while reading a policy file
|
||||
:raises yaml.YAMLError: Raised if parsing a policy file fails
|
||||
|
||||
:returns: List of services with deferred restarts belonging to application.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
deferred_restart_events = filter(
|
||||
lambda e: e['action'] == 'restart', get_deferred_events())
|
||||
|
||||
deferred_restart_services = set()
|
||||
for restart_event in deferred_restart_events:
|
||||
if application:
|
||||
if (
|
||||
restart_event['policy_requestor_type'] != 'charm' or
|
||||
restart_event['policy_requestor_type'] == 'charm' and
|
||||
restart_event['policy_requestor_name'] == application
|
||||
):
|
||||
deferred_restart_services.add(restart_event['service'])
|
||||
else:
|
||||
deferred_restart_services.add(restart_event['service'])
|
||||
|
||||
return list(deferred_restart_services)
|
||||
|
||||
|
||||
def main():
|
||||
"""Check for services with deferred restarts."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Check for services with deferred restarts')
|
||||
parser.add_argument(
|
||||
'--application', help='Check services belonging to this application only')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
services = set(get_deferred_restart_services(args.application))
|
||||
|
||||
if len(services) == 0:
|
||||
print('OK: No deferred service restarts.')
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(
|
||||
'CRITICAL: Restarts are deferred for services: {}.'.format(', '.join(services)))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except OSError as e:
|
||||
print('CRITICAL: A system error occurred: {} ({})'.format(e.errno, e.strerror))
|
||||
sys.exit(1)
|
||||
except yaml.YAMLError as e:
|
||||
print('CRITICAL: Failed to parse a policy file: {}'.format(str(e)))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print('CRITICAL: An unknown error occurred: {}'.format(str(e)))
|
||||
sys.exit(1)
|
@ -1,4 +1,3 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2017 Canonical Ltd
|
||||
#
|
||||
@ -14,7 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import six
|
||||
from charmhelpers.fetch import apt_install
|
||||
from charmhelpers.contrib.openstack.context import IdentityServiceContext
|
||||
from charmhelpers.core.hookenv import (
|
||||
@ -117,10 +115,7 @@ class KeystoneManager2(KeystoneManager):
|
||||
from keystoneclient.auth.identity import v2
|
||||
from keystoneclient import session
|
||||
except ImportError:
|
||||
if six.PY2:
|
||||
apt_install(["python-keystoneclient"], fatal=True)
|
||||
else: |