Pre-freeze 'make *-sync'

Change-Id: I7f029e37e4743587912c3264a38556caf8e00b3a
This commit is contained in:
Aurelien Lourot 2020-05-06 18:35:22 +02:00
parent 6afe8288a8
commit f02caf0743
4 changed files with 945 additions and 26 deletions

View File

@ -13,13 +13,17 @@
# limitations under the License.
import collections
import copy
import enum
import glob
import hashlib
import json
import math
import os
import re
import socket
import time
from base64 import b64decode
from subprocess import check_call, CalledProcessError
@ -50,7 +54,8 @@ from charmhelpers.core.hookenv import (
INFO,
ERROR,
status_set,
network_get_primary_address
network_get_primary_address,
WARNING,
)
from charmhelpers.core.sysctl import create as sysctl_create
@ -110,6 +115,13 @@ 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
try:
import psutil
except ImportError:
@ -263,6 +275,12 @@ class SharedDBContext(OSContextGenerator):
'database_password': rdata.get(password_setting),
'database_type': 'mysql+pymysql'
}
# Port is being introduced with LP Bug #1876188
# but it not currently required and may not be set in all
# cases, particularly in classic charms.
port = rdata.get('db_port')
if port:
ctxt['database_port'] = port
if CompareOpenStackReleases(rel) < 'queens':
ctxt['database_type'] = 'mysql'
if self.context_complete(ctxt):
@ -2396,3 +2414,734 @@ class DHCPAgentContext(OSContextGenerator):
return False
else:
return _config
EntityMac = collections.namedtuple('EntityMac', ['entity', 'mac'])
def resolve_pci_from_mapping_config(config_key):
"""Resolve local PCI devices from MAC addresses in mapping config.
Note that this function keeps record of mac->PCI address lookups
in the local unit db as the devices will disappaear from the system
once bound.
:param config_key: Configuration option key to parse data from
:type config_key: str
:returns: PCI device address to Tuple(entity, mac) map
:rtype: collections.OrderedDict[str,Tuple[str,str]]
"""
devices = pci.PCINetDevices()
resolved_devices = collections.OrderedDict()
db = kv()
# Note that ``parse_data_port_mappings`` returns Dict regardless of input
for mac, entity in parse_data_port_mappings(config(config_key)).items():
pcidev = devices.get_device_from_mac(mac)
if pcidev:
# NOTE: store mac->pci allocation as post binding
# it disappears from PCIDevices.
db.set(mac, pcidev.pci_address)
db.flush()
pci_address = db.get(mac)
if pci_address:
resolved_devices[pci_address] = EntityMac(entity, mac)
return resolved_devices
class DPDKDeviceContext(OSContextGenerator):
def __init__(self, driver_key=None, bridges_key=None, bonds_key=None):
"""Initialize DPDKDeviceContext.
:param driver_key: Key to use when retrieving driver config.
:type driver_key: str
:param bridges_key: Key to use when retrieving bridge config.
:type bridges_key: str
:param bonds_key: Key to use when retrieving bonds config.
:type bonds_key: str
"""
self.driver_key = driver_key or 'dpdk-driver'
self.bridges_key = bridges_key or 'data-port'
self.bonds_key = bonds_key or 'dpdk-bond-mappings'
def __call__(self):
"""Populate context.
:returns: context
:rtype: Dict[str,Union[str,collections.OrderedDict[str,str]]]
"""
driver = config(self.driver_key)
if driver is None:
return {}
# Resolve PCI devices for both directly used devices (_bridges)
# and devices for use in dpdk bonds (_bonds)
pci_devices = resolve_pci_from_mapping_config(self.bridges_key)
pci_devices.update(resolve_pci_from_mapping_config(self.bonds_key))
return {'devices': pci_devices,
'driver': driver}
class OVSDPDKDeviceContext(OSContextGenerator):
def __init__(self, bridges_key=None, bonds_key=None):
"""Initialize OVSDPDKDeviceContext.
:param bridges_key: Key to use when retrieving bridge config.
:type bridges_key: str
:param bonds_key: Key to use when retrieving bonds config.
:type bonds_key: str
"""
self.bridges_key = bridges_key or 'data-port'
self.bonds_key = bonds_key or 'dpdk-bond-mappings'
@staticmethod
def _parse_cpu_list(cpulist):
"""Parses a linux cpulist for a numa node
:returns: list of cores
:rtype: List[int]
"""
cores = []
ranges = cpulist.split(',')
for cpu_range in ranges:
if "-" in cpu_range:
cpu_min_max = cpu_range.split('-')
cores += range(int(cpu_min_max[0]),
int(cpu_min_max[1]) + 1)
else:
cores.append(int(cpu_range))
return cores
def _numa_node_cores(self):
"""Get map of numa node -> cpu core
:returns: map of numa node -> cpu core
:rtype: Dict[str,List[int]]
"""
nodes = {}
node_regex = '/sys/devices/system/node/node*'
for node in glob.glob(node_regex):
index = node.lstrip('/sys/devices/system/node/node')
with open(os.path.join(node, 'cpulist')) as cpulist:
nodes[index] = self._parse_cpu_list(cpulist.read().strip())
return nodes
def cpu_mask(self):
"""Get hex formatted CPU mask
The mask is based on using the first config:dpdk-socket-cores
cores of each NUMA node in the unit.
:returns: hex formatted CPU mask
:rtype: str
"""
num_cores = config('dpdk-socket-cores')
mask = 0
for cores in self._numa_node_cores().values():
for core in cores[:num_cores]:
mask = mask | 1 << core
return format(mask, '#04x')
def socket_memory(self):
"""Formatted list of socket memory configuration per NUMA node
:returns: socket memory configuration per NUMA node
:rtype: str
"""
sm_size = config('dpdk-socket-memory')
node_regex = '/sys/devices/system/node/node*'
mem_list = [str(sm_size) for _ in glob.glob(node_regex)]
if mem_list:
return ','.join(mem_list)
else:
return str(sm_size)
def devices(self):
"""List of PCI devices for use by DPDK
:returns: List of PCI devices for use by DPDK
:rtype: collections.OrderedDict[str,str]
"""
pci_devices = resolve_pci_from_mapping_config(self.bridges_key)
pci_devices.update(resolve_pci_from_mapping_config(self.bonds_key))
return pci_devices
def _formatted_whitelist(self, flag):
"""Flag formatted list of devices to whitelist
:param flag: flag format to use
:type flag: str
:rtype: str
"""
whitelist = []
for device in self.devices():
whitelist.append(flag.format(device=device))
return ' '.join(whitelist)
def device_whitelist(self):
"""Formatted list of devices to whitelist for dpdk
using the old style '-w' flag
:returns: devices to whitelist prefixed by '-w '
:rtype: str
"""
return self._formatted_whitelist('-w {device}')
def pci_whitelist(self):
"""Formatted list of devices to whitelist for dpdk
using the new style '--pci-whitelist' flag
:returns: devices to whitelist prefixed by '--pci-whitelist '
:rtype: str
"""
return self._formatted_whitelist('--pci-whitelist {device}')
def __call__(self):
"""Populate context.
:returns: context
:rtype: Dict[str,Union[bool,str]]
"""
ctxt = {}
whitelist = self.device_whitelist()
if whitelist:
ctxt['dpdk_enabled'] = config('enable-dpdk')
ctxt['device_whitelist'] = self.device_whitelist()
ctxt['socket_memory'] = self.socket_memory()
ctxt['cpu_mask'] = self.cpu_mask()
return ctxt
class BridgePortInterfaceMap(object):
"""Build a map of bridge ports and interaces from charm configuration.
NOTE: the handling of this detail in the charm is pre-deprecated.
The long term goal is for network connectivity detail to be modelled in
the server provisioning layer (such as MAAS) which in turn will provide
a Netplan YAML description that will be used to drive Open vSwitch.
Until we get to that reality the charm will need to configure this
detail based on application level configuration options.
There is a established way of mapping interfaces to ports and bridges
in the ``neutron-openvswitch`` and ``neutron-gateway`` charms and we
will carry that forward.
The relationship between bridge, port and interface(s).
+--------+
| bridge |
+--------+
|
+----------------+
| port aka. bond |
+----------------+
| |
+-+ +-+
|i| |i|
|n| |n|
|t| |t|
|0| |N|
+-+ +-+
"""
class interface_type(enum.Enum):
"""Supported interface types.
Supported interface types can be found in the ``iface_types`` column
in the ``Open_vSwitch`` table on a running system.
"""
dpdk = 'dpdk'
internal = 'internal'
system = 'system'
def __str__(self):
"""Return string representation of value.
:returns: string representation of value.
:rtype: str
"""
return self.value
def __init__(self, bridges_key=None, bonds_key=None, enable_dpdk_key=None,
global_mtu=None):
"""Initialize map.
:param bridges_key: Name of bridge:interface/port map config key
(default: 'data-port')
:type bridges_key: Optional[str]
:param bonds_key: Name of port-name:interface map config key
(default: 'dpdk-bond-mappings')
:type bonds_key: Optional[str]
:param enable_dpdk_key: Name of DPDK toggle config key
(default: 'enable-dpdk')
:type enable_dpdk_key: Optional[str]
:param global_mtu: Set a MTU on all interfaces at map initialization.
The default is to have Open vSwitch get this from the underlying
interface as set up by bare metal provisioning.
Note that you can augment the MTU on an individual interface basis
like this:
ifdatamap = bpi.get_ifdatamap(bridge, port)
ifdatamap = {
port: {
**ifdata,
**{'mtu-request': my_individual_mtu_map[port]},
}
for port, ifdata in ifdatamap.items()
}
:type global_mtu: Optional[int]
"""
bridges_key = bridges_key or 'data-port'
bonds_key = bonds_key or 'dpdk-bond-mappings'
enable_dpdk_key = enable_dpdk_key or 'enable-dpdk'
self._map = collections.defaultdict(
lambda: collections.defaultdict(dict))
self._ifname_mac_map = collections.defaultdict(list)
self._mac_ifname_map = {}
self._mac_pci_address_map = {}
# First we iterate over the list of physical interfaces visible to the
# system and update interface name to mac and mac to interface name map
for ifname in list_nics():
if not is_phy_iface(ifname):
continue
mac = get_nic_hwaddr(ifname)
self._ifname_mac_map[ifname] = [mac]
self._mac_ifname_map[mac] = ifname
# In light of the pre-deprecation notice in the docstring of this
# class we will expose the ability to configure OVS bonds as a
# DPDK-only feature, but generally use the data structures internally.
if config(enable_dpdk_key):
# resolve PCI address of interfaces listed in the bridges and bonds
# charm configuration options. Note that for already bound
# interfaces the helper will retrieve MAC address from the unit
# KV store as the information is no longer available in sysfs.
_pci_bridge_mac = resolve_pci_from_mapping_config(
bridges_key)
_pci_bond_mac = resolve_pci_from_mapping_config(
bonds_key)
for pci_address, bridge_mac in _pci_bridge_mac.items():
if bridge_mac.mac in self._mac_ifname_map:
# if we already have the interface name in our map it is
# visible to the system and therefore not bound to DPDK
continue
ifname = 'dpdk-{}'.format(
hashlib.sha1(
pci_address.encode('UTF-8')).hexdigest()[:7])
self._ifname_mac_map[ifname] = [bridge_mac.mac]
self._mac_ifname_map[bridge_mac.mac] = ifname
self._mac_pci_address_map[bridge_mac.mac] = pci_address
for pci_address, bond_mac in _pci_bond_mac.items():
# for bonds we want to be able to get a list of macs from
# the bond name and also get at the interface name made up
# of the hash of the PCI address
ifname = 'dpdk-{}'.format(
hashlib.sha1(
pci_address.encode('UTF-8')).hexdigest()[:7])
self._ifname_mac_map[bond_mac.entity].append(bond_mac.mac)
self._mac_ifname_map[bond_mac.mac] = ifname
self._mac_pci_address_map[bond_mac.mac] = pci_address
config_bridges = config(bridges_key) or ''
for bridge, ifname_or_mac in (
pair.split(':', 1)
for pair in config_bridges.split()):
if ':' in ifname_or_mac:
try:
ifname = self.ifname_from_mac(ifname_or_mac)
except KeyError:
# The interface is destined for a different unit in the
# deployment.
continue
macs = [ifname_or_mac]
else:
ifname = ifname_or_mac
macs = self.macs_from_ifname(ifname_or_mac)
portname = ifname
for mac in macs:
try:
pci_address = self.pci_address_from_mac(mac)
iftype = self.interface_type.dpdk
ifname = self.ifname_from_mac(mac)
except KeyError:
pci_address = None
iftype = self.interface_type.system
self.add_interface(
bridge, portname, ifname, iftype, pci_address, global_mtu)
def __getitem__(self, key):
"""Provide a Dict-like interface, get value of item.
:param key: Key to look up value from.
:type key: any
:returns: Value
:rtype: any
"""
return self._map.__getitem__(key)
def __iter__(self):
"""Provide a Dict-like interface, iterate over keys.
:returns: Iterator
:rtype: Iterator[any]
"""
return self._map.__iter__()
def __len__(self):
"""Provide a Dict-like interface, measure the length of internal map.
:returns: Length
:rtype: int
"""
return len(self._map)
def items(self):
"""Provide a Dict-like interface, iterate over items.
:returns: Key Value pairs
:rtype: Iterator[any, any]
"""
return self._map.items()
def keys(self):
"""Provide a Dict-like interface, iterate over keys.
:returns: Iterator
:rtype: Iterator[any]
"""
return self._map.keys()
def ifname_from_mac(self, mac):
"""
:returns: Name of interface
:rtype: str
:raises: KeyError
"""
return (get_bond_master(self._mac_ifname_map[mac]) or
self._mac_ifname_map[mac])
def macs_from_ifname(self, ifname):
"""
:returns: List of hardware address (MAC) of interface
:rtype: List[str]
:raises: KeyError
"""
return self._ifname_mac_map[ifname]
def pci_address_from_mac(self, mac):
"""
:param mac: Hardware address (MAC) of interface
:type mac: str
:returns: PCI address of device associated with mac
:rtype: str
:raises: KeyError
"""
return self._mac_pci_address_map[mac]
def add_interface(self, bridge, port, ifname, iftype,
pci_address, mtu_request):
"""Add an interface to the map.
:param bridge: Name of bridge on which the bond will be added
:type bridge: str
:param port: Name of port which will represent the bond on bridge
:type port: str
:param ifname: Name of interface that will make up the bonded port
:type ifname: str
:param iftype: Type of interface
:type iftype: BridgeBondMap.interface_type
:param pci_address: PCI address of interface
:type pci_address: Optional[str]
:param mtu_request: MTU to request for interface
:type mtu_request: Optional[int]
"""
self._map[bridge][port][ifname] = {
'type': str(iftype),
}
if pci_address:
self._map[bridge][port][ifname].update({
'pci-address': pci_address,
})
if mtu_request is not None:
self._map[bridge][port][ifname].update({
'mtu-request': str(mtu_request)
})
def get_ifdatamap(self, bridge, port):
"""Get structure suitable for charmhelpers.contrib.network.ovs helpers.
:param bridge: Name of bridge on which the port will be added
:type bridge: str
:param port: Name of port which will represent one or more interfaces
:type port: str
"""
for _bridge, _ports in self.items():
for _port, _interfaces in _ports.items():
if _bridge == bridge and _port == port:
ifdatamap = {}
for name, data in _interfaces.items():
ifdatamap.update({
name: {
'type': data['type'],
},
})
if data.get('mtu-request') is not None:
ifdatamap[name].update({
'mtu_request': data['mtu-request'],
})
if data.get('pci-address'):
ifdatamap[name].update({
'options': {
'dpdk-devargs': data['pci-address'],
},
})
return ifdatamap
class BondConfig(object):
"""Container and helpers for bond configuration options.
Data is put into a dictionary and a convenient config get interface is
provided.
"""
DEFAULT_LACP_CONFIG = {
'mode': 'balance-tcp',
'lacp': 'active',
'lacp-time': 'fast'
}
ALL_BONDS = 'ALL_BONDS'
BOND_MODES = ['active-backup', 'balance-slb', 'balance-tcp']
BOND_LACP = ['active', 'passive', 'off']
BOND_LACP_TIME = ['fast', 'slow']
def __init__(self, config_key=None):
"""Parse specified configuration option.
:param config_key: Configuration key to retrieve data from
(default: ``dpdk-bond-config``)
:type config_key: Optional[str]
"""
self.config_key = config_key or 'dpdk-bond-config'
self.lacp_config = {
self.ALL_BONDS: copy.deepcopy(self.DEFAULT_LACP_CONFIG)
}
lacp_config = config(self.config_key)
if lacp_config:
lacp_config_map = lacp_config.split()
for entry in lacp_config_map:
bond, entry = entry.partition(':')[0:3:2]
if not bond:
bond = self.ALL_BONDS
mode, entry = entry.partition(':')[0:3:2]
if not mode:
mode = self.DEFAULT_LACP_CONFIG['mode']
assert mode in self.BOND_MODES, \
"Bond mode {} is invalid".format(mode)
lacp, entry = entry.partition(':')[0:3:2]
if not lacp:
lacp = self.DEFAULT_LACP_CONFIG['lacp']
assert lacp in self.BOND_LACP, \
"Bond lacp {} is invalid".format(lacp)
lacp_time, entry = entry.partition(':')[0:3:2]
if not lacp_time:
lacp_time = self.DEFAULT_LACP_CONFIG['lacp-time']
assert lacp_time in self.BOND_LACP_TIME, \
"Bond lacp-time {} is invalid".format(lacp_time)
self.lacp_config[bond] = {
'mode': mode,
'lacp': lacp,
'lacp-time': lacp_time
}
def get_bond_config(self, bond):
"""Get the LACP configuration for a bond
:param bond: the bond name
:return: a dictionary with the configuration of the bond
:rtype: Dict[str,Dict[str,str]]
"""
return self.lacp_config.get(bond, self.lacp_config[self.ALL_BONDS])
def get_ovs_portdata(self, bond):
"""Get structure suitable for charmhelpers.contrib.network.ovs helpers.
:param bond: the bond name
:return: a dictionary with the configuration of the bond
:rtype: Dict[str,Union[str,Dict[str,str]]]
"""
bond_config = self.get_bond_config(bond)
return {
'bond_mode': bond_config['mode'],
'lacp': bond_config['lacp'],
'other_config': {
'lacp-time': bond_config['lacp-time'],
},
}
class SRIOVContext(OSContextGenerator):
"""Provide context for configuring SR-IOV devices."""
class sriov_config_mode(enum.Enum):
"""Mode in which SR-IOV is configured.
The configuration option identified by the ``numvfs_key`` parameter
is overloaded and defines in which mode the charm should interpret
the other SR-IOV-related configuration options.
"""
auto = 'auto'
blanket = 'blanket'
explicit = 'explicit'
def _determine_numvfs(self, device, sriov_numvfs):
"""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
:param sriov_numvfs: Number of VFs requested for blanket configuration.
:type sriov_numvfs: int
:returns: Number of VFs to configure for device
:rtype: Optional[int]
"""
def _get_capped_numvfs(requested):
"""Get a number of VFs that does not exceed individual card limits.
Depending and make and model of NIC the number of VFs supported
vary. Requesting more VFs than a card support would be a fatal
error, cap the requested number at the total number of VFs each
individual card supports.
:param requested: Number of VFs requested
:type requested: int
:returns: Number of VFs allowed
:rtype: int
"""
actual = min(int(requested), int(device.sriov_totalvfs))
if actual < int(requested):
log('Requested VFs ({}) too high for device {}. Falling back '
'to value supprted by device: {}'
.format(requested, device.interface_name,
device.sriov_totalvfs),
level=WARNING)
return actual
if self._sriov_config_mode == self.sriov_config_mode.auto:
# auto-mode
#
# If device mapping configuration is present, return information
# on cards with mapping.
#
# If no device mapping configuration is present, return information
# for all cards.
#
# The maximum number of VFs supported by card will be used.
if (self._sriov_mapped_devices and
device.interface_name not in self._sriov_mapped_devices):
log('SR-IOV configured in auto mode: No device mapping for {}'
.format(device.interface_name),
level=DEBUG)
return
return _get_capped_numvfs(device.sriov_totalvfs)
elif self._sriov_config_mode == self.sriov_config_mode.blanket:
# blanket-mode
#
# User has specified a number of VFs that should apply to all
# cards with support for VFs.
return _get_capped_numvfs(sriov_numvfs)
elif self._sriov_config_mode == self.sriov_config_mode.explicit:
# explicit-mode
#
# User has given a list of interface names and associated number of
# VFs
if device.interface_name not in self._sriov_config_devices:
log('SR-IOV configured in explicit mode: No device:numvfs '
'pair for device {}, skipping.'
.format(device.interface_name),
level=DEBUG)
return
return _get_capped_numvfs(
self._sriov_config_devices[device.interface_name])
else:
raise RuntimeError('This should not be reached')
def __init__(self, numvfs_key=None, device_mappings_key=None):
"""Initialize map from PCI devices and configuration options.
:param numvfs_key: Config key for numvfs (default: 'sriov-numvfs')
:type numvfs_key: Optional[str]
:param device_mappings_key: Config key for device mappings
(default: 'sriov-device-mappings')
:type device_mappings_key: Optional[str]
:raises: RuntimeError
"""
numvfs_key = numvfs_key or 'sriov-numvfs'
device_mappings_key = device_mappings_key or 'sriov-device-mappings'
devices = pci.PCINetDevices()
charm_config = config()
sriov_numvfs = charm_config.get(numvfs_key) or ''
sriov_device_mappings = charm_config.get(device_mappings_key) or ''
# create list of devices from sriov_device_mappings config option
self._sriov_mapped_devices = [
pair.split(':', 1)[1]
for pair in sriov_device_mappings.split()
]
# create map of device:numvfs from sriov_numvfs config option
self._sriov_config_devices = {
ifname: numvfs for ifname, numvfs in (
pair.split(':', 1) for pair in sriov_numvfs.split()
if ':' in sriov_numvfs)
}
# determine configuration mode from contents of sriov_numvfs
if sriov_numvfs == 'auto':
self._sriov_config_mode = self.sriov_config_mode.auto
elif sriov_numvfs.isdigit():
self._sriov_config_mode = self.sriov_config_mode.blanket
elif ':' in sriov_numvfs:
self._sriov_config_mode = self.sriov_config_mode.explicit
else:
raise RuntimeError('Unable to determine mode of SR-IOV '
'configuration.')
self._map = {
device.interface_name: self._determine_numvfs(device, sriov_numvfs)
for device in devices.pci_devices
if device.sriov and
self._determine_numvfs(device, sriov_numvfs) is not None
}
def __call__(self):
"""Provide SR-IOV context.
:returns: Map interface name: min(configured, max) virtual functions.
Example:
{
'eth0': 16,
'eth1': 32,
'eth2': 64,
}
:rtype: Dict[str,int]
"""
return self._map

View File

@ -163,7 +163,16 @@ def retrieve_secret_id(url, token):
:returns: secret_id to use for Vault Access
:rtype: str"""
import hvac
client = hvac.Client(url=url, token=token)
try:
# hvac 0.10.1 changed default adapter to JSONAdapter
client = hvac.Client(url=url, token=token, adapter=hvac.adapters.Request)
except AttributeError:
# hvac < 0.6.2 doesn't have adapter but uses the same response interface
client = hvac.Client(url=url, token=token)
else:
# hvac < 0.9.2 assumes adapter is an instance, so doesn't instantiate
if not isinstance(client.adapter, hvac.adapters.Request):
client.adapter = hvac.adapters.Request(base_uri=url, token=token)
response = client._post('/v1/sys/wrapping/unwrap')
if response.status_code == 200:
data = response.json()

View File

@ -160,9 +160,10 @@ def handle_create_erasure_profile(request, service):
# "host" | "rack" or it defaults to "host" # Any valid Ceph bucket
failure_domain = request.get('failure-domain')
name = request.get('name')
k = request.get('k')
m = request.get('m')
l = request.get('l')
# Binary Distribution Matrix (BDM) parameters
bdm_k = request.get('k')
bdm_m = request.get('m')
bdm_l = request.get('l')
if failure_domain not in CEPH_BUCKET_TYPES:
msg = "failure-domain must be one of {}".format(CEPH_BUCKET_TYPES)
@ -171,7 +172,8 @@ def handle_create_erasure_profile(request, service):
create_erasure_profile(service=service, erasure_plugin_name=erasure_type,
profile_name=name, failure_domain=failure_domain,
data_chunks=k, coding_chunks=m, locality=l)
data_chunks=bdm_k, coding_chunks=bdm_m,
locality=bdm_l)
def handle_add_permissions_to_key(request, service):
@ -556,7 +558,7 @@ def handle_set_pool_value(request, service):
# Get the validation method
validator_params = POOL_KEYS[params['key']]
if len(validator_params) is 1:
if len(validator_params) == 1:
# Validate that what the user passed is actually legal per Ceph's rules
validator(params['value'], validator_params[0])
else:

View File

@ -25,6 +25,7 @@ import sys
import time
import uuid
from contextlib import contextmanager
from datetime import datetime
from charmhelpers.core import hookenv
@ -175,12 +176,16 @@ def unmounted_disks():
context = pyudev.Context()
for device in context.list_devices(DEVTYPE='disk'):
if device['SUBSYSTEM'] == 'block':
if device.device_node is None:
continue
matched = False
for block_type in [u'dm-', u'loop', u'ram', u'nbd']:
if block_type in device.device_node:
matched = True
if matched:
continue
disks.append(device.device_node)
log("Found disks: {}".format(disks))
return [disk for disk in disks if not is_device_mounted(disk)]
@ -637,7 +642,7 @@ def _get_osd_num_from_dirname(dirname):
:raises ValueError: if the osd number cannot be parsed from the provided
directory name.
"""
match = re.search('ceph-(?P<osd_id>\d+)', dirname)
match = re.search(r'ceph-(?P<osd_id>\d+)', dirname)
if not match:
raise ValueError("dirname not in correct format: {}".format(dirname))
@ -706,7 +711,7 @@ def get_version():
package = "ceph"
try:
pkg = cache[package]
except:
except KeyError:
# the package is unknown to the current apt cache.
e = 'Could not determine version of package with no installation ' \
'candidate: %s' % package
@ -721,7 +726,7 @@ def get_version():
# x.y match only for 20XX.X
# and ignore patch level for other packages
match = re.match('^(\d+)\.(\d+)', vers)
match = re.match(r'^(\d+)\.(\d+)', vers)
if match:
vers = match.group(0)
@ -956,11 +961,11 @@ def start_osds(devices):
rescan_osd_devices()
if (cmp_pkgrevno('ceph', '0.56.6') >= 0 and
cmp_pkgrevno('ceph', '14.2.0') < 0):
# Use ceph-disk activate for directory based OSD's
for dev_or_path in devices:
if os.path.exists(dev_or_path) and os.path.isdir(dev_or_path):
subprocess.check_call(
['ceph-disk', 'activate', dev_or_path])
# Use ceph-disk activate for directory based OSD's
for dev_or_path in devices:
if os.path.exists(dev_or_path) and os.path.isdir(dev_or_path):
subprocess.check_call(
['ceph-disk', 'activate', dev_or_path])
def udevadm_settle():
@ -978,6 +983,7 @@ def rescan_osd_devices():
udevadm_settle()
_client_admin_keyring = '/etc/ceph/ceph.client.admin.keyring'
@ -1002,6 +1008,7 @@ def generate_monitor_secret():
return "{}==".format(res.split('=')[1].strip())
# OSD caps taken from ceph-create-keys
_osd_bootstrap_caps = {
'mon': [
@ -1039,7 +1046,7 @@ def get_osd_bootstrap_key():
# Attempt to get/create a key using the OSD bootstrap profile first
key = get_named_key('bootstrap-osd',
_osd_bootstrap_caps_profile)
except:
except Exception:
# If that fails try with the older style permissions
key = get_named_key('bootstrap-osd',
_osd_bootstrap_caps)
@ -1063,6 +1070,7 @@ def import_radosgw_key(key):
]
subprocess.check_call(cmd)
# OSD caps taken from ceph-create-keys
_radosgw_caps = {
'mon': ['allow rw'],
@ -1300,7 +1308,7 @@ def bootstrap_monitor_cluster(secret):
path,
done,
init_marker)
except:
except Exception:
raise
finally:
os.unlink(keyring)
@ -2417,10 +2425,11 @@ def upgrade_osd(new_version):
# way to update the code on the node.
if not dirs_need_ownership_update('osd'):
log('Restarting all OSDs to load new binaries', DEBUG)
if systemd():
service_restart('ceph-osd.target')
else:
service_restart('ceph-osd-all')
with maintain_all_osd_states():
if systemd():
service_restart('ceph-osd.target')
else:
service_restart('ceph-osd-all')
return
# Need to change the ownership of all directories which are not OSD
@ -2465,11 +2474,12 @@ def _upgrade_single_osd(osd_num, osd_dir):
:raises IOError: if an error occurs reading/writing to a file as part
of the upgrade process
"""
stop_osd(osd_num)
disable_osd(osd_num)
update_owner(osd_dir)
enable_osd(osd_num)
start_osd(osd_num)
with maintain_osd_state(osd_num):
stop_osd(osd_num)
disable_osd(osd_num)
update_owner(osd_dir)
enable_osd(osd_num)
start_osd(osd_num)
def stop_osd(osd_num):
@ -2596,6 +2606,98 @@ def update_owner(path, recurse_dirs=True):
secs=elapsed_time.total_seconds(), path=path), DEBUG)
def get_osd_state(osd_num, osd_goal_state=None):
"""Get OSD state or loop until OSD state matches OSD goal state.
If osd_goal_state is None, just return the current OSD state.
If osd_goal_state is not None, loop until the current OSD state matches
the OSD goal state.
:param osd_num: the osd id to get state for
:param osd_goal_state: (Optional) string indicating state to wait for
Defaults to None
:returns: Returns a str, the OSD state.
:rtype: str
"""
while True:
asok = "/var/run/ceph/ceph-osd.{}.asok".format(osd_num)
cmd = [
'ceph',
'daemon',
asok,
'status'
]
try:
result = json.loads(str(subprocess
.check_output(cmd)
.decode('UTF-8')))
except (subprocess.CalledProcessError, ValueError) as e:
log("{}".format(e), level=DEBUG)
continue
osd_state = result['state']
log("OSD {} state: {}, goal state: {}".format(
osd_num, osd_state, osd_goal_state), level=DEBUG)
if not osd_goal_state:
return osd_state
if osd_state == osd_goal_state:
return osd_state
time.sleep(3)
def get_all_osd_states(osd_goal_states=None):
"""Get all OSD states or loop until all OSD states match OSD goal states.
If osd_goal_states is None, just return a dictionary of current OSD states.
If osd_goal_states is not None, loop until the current OSD states match
the OSD goal states.
:param osd_goal_states: (Optional) dict indicating states to wait for
Defaults to None
:returns: Returns a dictionary of current OSD states.
:rtype: dict
"""
osd_states = {}
for osd_num in get_local_osd_ids():
if not osd_goal_states:
osd_states[osd_num] = get_osd_state(osd_num)
else:
osd_states[osd_num] = get_osd_state(
osd_num,
osd_goal_state=osd_goal_states[osd_num])
return osd_states
@contextmanager
def maintain_osd_state(osd_num):
"""Ensure the state of an OSD is maintained.
Ensures the state of an OSD is the same at the end of a block nested
in a with statement as it was at the beginning of the block.
:param osd_num: the osd id to maintain state for
"""
osd_state = get_osd_state(osd_num)
try:
yield
finally:
get_osd_state(osd_num, osd_goal_state=osd_state)
@contextmanager
def maintain_all_osd_states():
"""Ensure all local OSD states are maintained.
Ensures the states of all local OSDs are the same at the end of a
block nested in a with statement as they were at the beginning of
the block.
"""
osd_states = get_all_osd_states()
try:
yield
finally:
get_all_osd_states(osd_goal_states=osd_states)
def list_pools(client='admin'):
"""This will list the current pools that Ceph has
@ -2790,6 +2892,7 @@ def dirs_need_ownership_update(service):
# All child directories had the expected ownership
return False
# A dict of valid ceph upgrade paths. Mapping is old -> new
UPGRADE_PATHS = collections.OrderedDict([
('firefly', 'hammer'),
@ -2797,6 +2900,7 @@ UPGRADE_PATHS = collections.OrderedDict([
('jewel', 'luminous'),
('luminous', 'mimic'),
('mimic', 'nautilus'),
('nautilus', 'octopus'),
])
# Map UCA codenames to ceph codenames
@ -2813,6 +2917,7 @@ UCA_CODENAME_MAP = {
'rocky': 'mimic',
'stein': 'mimic',
'train': 'nautilus',
'ussuri': 'octopus',
}
@ -2965,3 +3070,57 @@ def osd_noout(enable):
except subprocess.CalledProcessError as e:
log(e)
raise
class OSDConfigSetError(Exception):
"""Error occured applying OSD settings."""
pass
def apply_osd_settings(settings):
"""Applies the provided osd settings
Apply the provided settings to all local OSD unless settings are already
present. Settings stop being applied on encountering an error.
:param settings: dict. Dictionary of settings to apply.
:returns: bool. True if commands ran succesfully.
:raises: OSDConfigSetError
"""
current_settings = {}
base_cmd = 'ceph daemon osd.{osd_id} config --format=json'
get_cmd = base_cmd + ' get {key}'
set_cmd = base_cmd + ' set {key} {value}'
def _get_cli_key(key):
return(key.replace(' ', '_'))
# Retrieve the current values to check keys are correct and to make this a
# noop if setting are already applied.
for osd_id in get_local_osd_ids():
for key, value in sorted(settings.items()):
cli_key = _get_cli_key(key)
cmd = get_cmd.format(osd_id=osd_id, key=cli_key)
out = json.loads(
subprocess.check_output(cmd.split()).decode('UTF-8'))
if 'error' in out:
log("Error retrieving osd setting: {}".format(out['error']),
level=ERROR)
return False
current_settings[key] = out[cli_key]
settings_diff = {
k: v
for k, v in settings.items()
if str(v) != str(current_settings[k])}
for key, value in sorted(settings_diff.items()):
log("Setting {} to {}".format(key, value), level=DEBUG)
cmd = set_cmd.format(
osd_id=osd_id,
key=_get_cli_key(key),
value=value)
out = json.loads(
subprocess.check_output(cmd.split()).decode('UTF-8'))
if 'error' in out:
log("Error applying osd setting: {}".format(out['error']),
level=ERROR)
raise OSDConfigSetError
return True