No reformat

Do not reformat devices.  A subsequent change will be necessary
to account for conditions where a reformat is still desired,
such as a set of blocking states and user-driven actions.

Partial-bug: #1698154

Depends-On: I90a866aa138d18e4242783c42d4c7c587f696d7d
Change-Id: I3a41ab38e7a1679cf4f5380a7cc56556da3aaf2b
This commit is contained in:
Ryan Beisner 2018-05-31 07:00:33 -05:00 committed by Frode Nordahl
parent 2c3eae272f
commit 22ce311b0b
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
14 changed files with 312 additions and 70 deletions

View File

@ -31,7 +31,6 @@ import ceph.utils
def add_device(request, device_path, bucket=None): def add_device(request, device_path, bucket=None):
ceph.utils.osdize(dev, hookenv.config('osd-format'), ceph.utils.osdize(dev, hookenv.config('osd-format'),
ceph_hooks.get_journal_devices(), ceph_hooks.get_journal_devices(),
hookenv.config('osd-reformat'),
hookenv.config('ignore-device-errors'), hookenv.config('ignore-device-errors'),
hookenv.config('osd-encrypt'), hookenv.config('osd-encrypt'),
hookenv.config('bluestore')) hookenv.config('bluestore'))

View File

@ -127,17 +127,6 @@ options:
. .
Note that despite bluestore being the default for Ceph Luminous, if this Note that despite bluestore being the default for Ceph Luminous, if this
option is False, OSDs will still use filestore. option is False, OSDs will still use filestore.
osd-reformat:
type: boolean
default: False
description: |
By default, the charm will not re-format a device that already looks
as if it might be an OSD device. This is a safeguard to try to
prevent data loss.
.
Enabling this option forces a reformat of any OSD devices found which
have not been processed by the unit previously or are not already
mounted.
osd-encrypt: osd-encrypt:
type: boolean type: boolean
default: False default: False

View File

@ -451,7 +451,7 @@ def prepare_disks_and_activate():
emit_cephconf() emit_cephconf()
for dev in get_devices(): for dev in get_devices():
ceph.osdize(dev, config('osd-format'), ceph.osdize(dev, config('osd-format'),
osd_journal, config('osd-reformat'), osd_journal,
config('ignore-device-errors'), config('ignore-device-errors'),
config('osd-encrypt'), config('osd-encrypt'),
config('bluestore'), config('bluestore'),
@ -499,13 +499,6 @@ def get_conf(name):
return None return None
def reformat_osd():
if config('osd-reformat'):
return True
else:
return False
def get_devices(): def get_devices():
devices = [] devices = []
if config('osd-devices'): if config('osd-devices'):

View File

@ -223,6 +223,11 @@ def https():
return True return True
if config_get('ssl_cert') and config_get('ssl_key'): if config_get('ssl_cert') and config_get('ssl_key'):
return True 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 r_id in relation_ids('identity-service'):
for unit in relation_list(r_id): for unit in relation_list(r_id):
# TODO - needs fixing for new helper as ssl_cert/key suffixes with CN # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN

View File

@ -544,7 +544,7 @@ class OpenStackAmuletUtils(AmuletUtils):
return ep return ep
def get_default_keystone_session(self, keystone_sentry, def get_default_keystone_session(self, keystone_sentry,
openstack_release=None): openstack_release=None, api_version=2):
"""Return a keystone session object and client object assuming standard """Return a keystone session object and client object assuming standard
default settings default settings
@ -559,12 +559,12 @@ class OpenStackAmuletUtils(AmuletUtils):
eyc eyc
""" """
self.log.debug('Authenticating keystone admin...') self.log.debug('Authenticating keystone admin...')
api_version = 2
client_class = keystone_client.Client
# 11 => xenial_queens # 11 => xenial_queens
if openstack_release and openstack_release >= 11: if api_version == 3 or (openstack_release and openstack_release >= 11):
api_version = 3
client_class = keystone_client_v3.Client client_class = keystone_client_v3.Client
api_version = 3
else:
client_class = keystone_client.Client
keystone_ip = keystone_sentry.info['public-address'] keystone_ip = keystone_sentry.info['public-address']
session, auth = self.get_keystone_session( session, auth = self.get_keystone_session(
keystone_ip, keystone_ip,

View File

@ -0,0 +1,227 @@
# Copyright 2014-2018 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.
# Common python helper functions used for OpenStack charm certificats.
import os
import json
from charmhelpers.contrib.network.ip import (
get_hostname,
resolve_network_cidr,
)
from charmhelpers.core.hookenv import (
local_unit,
network_get_primary_address,
config,
relation_get,
unit_get,
NoNetworkBinding,
log,
WARNING,
)
from charmhelpers.contrib.openstack.ip import (
ADMIN,
resolve_address,
get_vip_in_network,
INTERNAL,
PUBLIC,
ADDRESS_MAP)
from charmhelpers.core.host import (
mkdir,
write_file,
)
from charmhelpers.contrib.hahelpers.apache import (
install_ca_cert
)
class CertRequest(object):
"""Create a request for certificates to be generated
"""
def __init__(self, json_encode=True):
self.entries = []
self.hostname_entry = None
self.json_encode = json_encode
def add_entry(self, net_type, cn, addresses):
"""Add a request to the batch
:param net_type: str netwrok space name request is for
:param cn: str Canonical Name for certificate
:param addresses: [] List of addresses to be used as SANs
"""
self.entries.append({
'cn': cn,
'addresses': addresses})
def add_hostname_cn(self):
"""Add a request for the hostname of the machine"""
ip = unit_get('private-address')
addresses = [ip]
# If a vip is being used without os-hostname config or
# network spaces then we need to ensure the local units
# cert has the approriate vip in the SAN list
vip = get_vip_in_network(resolve_network_cidr(ip))
if vip:
addresses.append(vip)
self.hostname_entry = {
'cn': get_hostname(ip),
'addresses': addresses}
def add_hostname_cn_ip(self, addresses):
"""Add an address to the SAN list for the hostname request
:param addr: [] List of address to be added
"""
for addr in addresses:
if addr not in self.hostname_entry['addresses']:
self.hostname_entry['addresses'].append(addr)
def get_request(self):
"""Generate request from the batched up entries
"""
if self.hostname_entry:
self.entries.append(self.hostname_entry)
request = {}
for entry in self.entries:
sans = sorted(list(set(entry['addresses'])))
request[entry['cn']] = {'sans': sans}
if self.json_encode:
return {'cert_requests': json.dumps(request, sort_keys=True)}
else:
return {'cert_requests': request}
def get_certificate_request(json_encode=True):
"""Generate a certificatee requests based on the network confioguration
"""
req = CertRequest(json_encode=json_encode)
req.add_hostname_cn()
# Add os-hostname entries
for net_type in [INTERNAL, ADMIN, PUBLIC]:
net_config = config(ADDRESS_MAP[net_type]['override'])
try:
net_addr = resolve_address(endpoint_type=net_type)
ip = network_get_primary_address(
ADDRESS_MAP[net_type]['binding'])
addresses = [net_addr, ip]
vip = get_vip_in_network(resolve_network_cidr(ip))
if vip:
addresses.append(vip)
if net_config:
req.add_entry(
net_type,
net_config,
addresses)
else:
# There is network address with no corresponding hostname.
# Add the ip to the hostname cert to allow for this.
req.add_hostname_cn_ip(addresses)
except NoNetworkBinding:
log("Skipping request for certificate for ip in {} space, no "
"local address found".format(net_type), WARNING)
return req.get_request()
def create_ip_cert_links(ssl_dir, custom_hostname_link=None):
"""Create symlinks for SAN records
:param ssl_dir: str Directory to create symlinks in
:param custom_hostname_link: str Additional link to be created
"""
hostname = get_hostname(unit_get('private-address'))
hostname_cert = os.path.join(
ssl_dir,
'cert_{}'.format(hostname))
hostname_key = os.path.join(
ssl_dir,
'key_{}'.format(hostname))
# Add links to hostname cert, used if os-hostname vars not set
for net_type in [INTERNAL, ADMIN, PUBLIC]:
try:
addr = resolve_address(endpoint_type=net_type)
cert = os.path.join(ssl_dir, 'cert_{}'.format(addr))
key = os.path.join(ssl_dir, 'key_{}'.format(addr))
if os.path.isfile(hostname_cert) and not os.path.isfile(cert):
os.symlink(hostname_cert, cert)
os.symlink(hostname_key, key)
except NoNetworkBinding:
log("Skipping creating cert symlink for ip in {} space, no "
"local address found".format(net_type), WARNING)
if custom_hostname_link:
custom_cert = os.path.join(
ssl_dir,
'cert_{}'.format(custom_hostname_link))
custom_key = os.path.join(
ssl_dir,
'key_{}'.format(custom_hostname_link))
if os.path.isfile(hostname_cert) and not os.path.isfile(custom_cert):
os.symlink(hostname_cert, custom_cert)
os.symlink(hostname_key, custom_key)
def install_certs(ssl_dir, certs, chain=None):
"""Install the certs passed into the ssl dir and append the chain if
provided.
:param ssl_dir: str Directory to create symlinks in
:param certs: {} {'cn': {'cert': 'CERT', 'key': 'KEY'}}
:param chain: str Chain to be appended to certs
"""
for cn, bundle in certs.items():
cert_filename = 'cert_{}'.format(cn)
key_filename = 'key_{}'.format(cn)
cert_data = bundle['cert']
if chain:
# Append chain file so that clients that trust the root CA will
# trust certs signed by an intermediate in the chain
cert_data = cert_data + chain
write_file(
path=os.path.join(ssl_dir, cert_filename),
content=cert_data, perms=0o640)
write_file(
path=os.path.join(ssl_dir, key_filename),
content=bundle['key'], perms=0o640)
def process_certificates(service_name, relation_id, unit,
custom_hostname_link=None):
"""Process the certificates supplied down the relation
:param service_name: str Name of service the certifcates are for.
:param relation_id: str Relation id providing the certs
:param unit: str Unit providing the certs
:param custom_hostname_link: str Name of custom link to create
"""
data = relation_get(rid=relation_id, unit=unit)
ssl_dir = os.path.join('/etc/apache2/ssl/', service_name)
mkdir(path=ssl_dir)
name = local_unit().replace('/', '_')
certs = data.get('{}.processed_requests'.format(name))
chain = data.get('chain')
ca = data.get('ca')
if certs:
certs = json.loads(certs)
install_ca_cert(ca.encode())
install_certs(ssl_dir, certs, chain)
create_ip_cert_links(
ssl_dir,
custom_hostname_link=custom_hostname_link)

View File

@ -789,17 +789,18 @@ class ApacheSSLContext(OSContextGenerator):
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace) ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
mkdir(path=ssl_dir) mkdir(path=ssl_dir)
cert, key = get_cert(cn) cert, key = get_cert(cn)
if cn: if cert and key:
cert_filename = 'cert_{}'.format(cn) if cn:
key_filename = 'key_{}'.format(cn) cert_filename = 'cert_{}'.format(cn)
else: key_filename = 'key_{}'.format(cn)
cert_filename = 'cert' else:
key_filename = 'key' cert_filename = 'cert'
key_filename = 'key'
write_file(path=os.path.join(ssl_dir, cert_filename), write_file(path=os.path.join(ssl_dir, cert_filename),
content=b64decode(cert), perms=0o640) content=b64decode(cert), perms=0o640)
write_file(path=os.path.join(ssl_dir, key_filename), write_file(path=os.path.join(ssl_dir, key_filename),
content=b64decode(key), perms=0o640) content=b64decode(key), perms=0o640)
def configure_ca(self): def configure_ca(self):
ca_cert = get_ca_cert() ca_cert = get_ca_cert()
@ -871,23 +872,31 @@ class ApacheSSLContext(OSContextGenerator):
if not self.external_ports or not https(): if not self.external_ports or not https():
return {} return {}
self.configure_ca() use_keystone_ca = True
for rid in relation_ids('certificates'):
if related_units(rid):
use_keystone_ca = False
if use_keystone_ca:
self.configure_ca()
self.enable_modules() self.enable_modules()
ctxt = {'namespace': self.service_namespace, ctxt = {'namespace': self.service_namespace,
'endpoints': [], 'endpoints': [],
'ext_ports': []} 'ext_ports': []}
cns = self.canonical_names() if use_keystone_ca:
if cns: cns = self.canonical_names()
for cn in cns: if cns:
self.configure_cert(cn) for cn in cns:
else: self.configure_cert(cn)
# Expect cert/key provided in config (currently assumed that ca else:
# uses ip for cn) # Expect cert/key provided in config (currently assumed that ca
for net_type in (INTERNAL, ADMIN, PUBLIC): # uses ip for cn)
cn = resolve_address(endpoint_type=net_type) for net_type in (INTERNAL, ADMIN, PUBLIC):
self.configure_cert(cn) cn = resolve_address(endpoint_type=net_type)
self.configure_cert(cn)
addresses = self.get_network_addresses() addresses = self.get_network_addresses()
for address, endpoint in addresses: for address, endpoint in addresses:

View File

@ -184,3 +184,13 @@ def resolve_address(endpoint_type=PUBLIC, override=True):
"clustered=%s)" % (net_type, clustered)) "clustered=%s)" % (net_type, clustered))
return resolved_address return resolved_address
def get_vip_in_network(network):
matching_vip = None
vips = config('vip')
if vips:
for vip in vips.split():
if is_address_in_network(network, vip):
matching_vip = vip
return matching_vip

View File

@ -972,6 +972,13 @@ def application_version_set(version):
log("Application Version: {}".format(version)) log("Application Version: {}".format(version))
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
def goal_state():
"""Juju goal state values"""
cmd = ['goal-state', '--format=json']
return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
@translate_exc(from_exc=OSError, to_exc=NotImplementedError) @translate_exc(from_exc=OSError, to_exc=NotImplementedError)
def is_leader(): def is_leader():
"""Does the current unit hold the juju leadership """Does the current unit hold the juju leadership

View File

@ -1366,20 +1366,18 @@ def get_devices(name):
return set(devices) return set(devices)
def osdize(dev, osd_format, osd_journal, reformat_osd=False, def osdize(dev, osd_format, osd_journal, ignore_errors=False, encrypt=False,
ignore_errors=False, encrypt=False, bluestore=False, bluestore=False, key_manager=CEPH_KEY_MANAGER):
key_manager=CEPH_KEY_MANAGER):
if dev.startswith('/dev'): if dev.startswith('/dev'):
osdize_dev(dev, osd_format, osd_journal, osdize_dev(dev, osd_format, osd_journal,
reformat_osd, ignore_errors, encrypt, ignore_errors, encrypt,
bluestore, key_manager) bluestore, key_manager)
else: else:
osdize_dir(dev, encrypt, bluestore) osdize_dir(dev, encrypt, bluestore)
def osdize_dev(dev, osd_format, osd_journal, reformat_osd=False, def osdize_dev(dev, osd_format, osd_journal, ignore_errors=False,
ignore_errors=False, encrypt=False, bluestore=False, encrypt=False, bluestore=False, key_manager=CEPH_KEY_MANAGER):
key_manager=CEPH_KEY_MANAGER):
""" """
Prepare a block device for use as a Ceph OSD Prepare a block device for use as a Ceph OSD
@ -1389,8 +1387,6 @@ def osdize_dev(dev, osd_format, osd_journal, reformat_osd=False,
:param: dev: Full path to block device to use :param: dev: Full path to block device to use
:param: osd_format: Format for OSD filesystem :param: osd_format: Format for OSD filesystem
:param: osd_journal: List of block devices to use for OSD journals :param: osd_journal: List of block devices to use for OSD journals
:param: reformat_osd: Reformat devices that are not currently in use
which have been used previously
:param: ignore_errors: Don't fail in the event of any errors during :param: ignore_errors: Don't fail in the event of any errors during
processing processing
:param: encrypt: Encrypt block devices using 'key_manager' :param: encrypt: Encrypt block devices using 'key_manager'
@ -1418,7 +1414,7 @@ def osdize_dev(dev, osd_format, osd_journal, reformat_osd=False,
log('Path {} is not a block device - bailing'.format(dev)) log('Path {} is not a block device - bailing'.format(dev))
return return
if is_osd_disk(dev) and not reformat_osd: if is_osd_disk(dev):
log('Looks like {} is already an' log('Looks like {} is already an'
' OSD data or journal, skipping.'.format(dev)) ' OSD data or journal, skipping.'.format(dev))
return return
@ -1432,9 +1428,6 @@ def osdize_dev(dev, osd_format, osd_journal, reformat_osd=False,
' skipping.'.format(dev)) ' skipping.'.format(dev))
return return
if reformat_osd:
zap_disk(dev)
if cmp_pkgrevno('ceph', '12.2.4') >= 0: if cmp_pkgrevno('ceph', '12.2.4') >= 0:
cmd = _ceph_volume(dev, cmd = _ceph_volume(dev,
osd_journal, osd_journal,

View File

@ -63,7 +63,10 @@ class CephOsdBasicDeployment(OpenStackAmuletDeployment):
and the rest of the service are from lp branches that are and the rest of the service are from lp branches that are
compatible with the local charm (e.g. stable or next). compatible with the local charm (e.g. stable or next).
""" """
this_service = {'name': 'ceph-osd', 'units': 3} this_service = {
'name': 'ceph-osd',
'units': 3,
'storage': {'osd-devices': 'cinder,10G'}}
other_services = [ other_services = [
{'name': 'ceph-mon', 'units': 3}, {'name': 'ceph-mon', 'units': 3},
{'name': 'percona-cluster'}, {'name': 'percona-cluster'},
@ -118,9 +121,7 @@ class CephOsdBasicDeployment(OpenStackAmuletDeployment):
# Include a non-existent device as osd-devices is a whitelist, # Include a non-existent device as osd-devices is a whitelist,
# and this will catch cases where proposals attempt to change that. # and this will catch cases where proposals attempt to change that.
ceph_osd_config = { ceph_osd_config = {
'osd-reformat': True, 'osd-devices': '/srv/ceph /dev/test-non-existent'
'ephemeral-unmount': '/mnt',
'osd-devices': '/dev/vdb /srv/ceph /dev/test-non-existent'
} }
configs = {'keystone': keystone_config, configs = {'keystone': keystone_config,

View File

@ -50,7 +50,8 @@ class AmuletDeployment(object):
this_service['units'] = 1 this_service['units'] = 1
self.d.add(this_service['name'], units=this_service['units'], self.d.add(this_service['name'], units=this_service['units'],
constraints=this_service.get('constraints')) constraints=this_service.get('constraints'),
storage=this_service.get('storage'))
for svc in other_services: for svc in other_services:
if 'location' in svc: if 'location' in svc:
@ -64,7 +65,8 @@ class AmuletDeployment(object):
svc['units'] = 1 svc['units'] = 1
self.d.add(svc['name'], charm=branch_location, units=svc['units'], self.d.add(svc['name'], charm=branch_location, units=svc['units'],
constraints=svc.get('constraints')) constraints=svc.get('constraints'),
storage=svc.get('storage'))
def _add_relations(self, relations): def _add_relations(self, relations):
"""Add all of the relations for the services.""" """Add all of the relations for the services."""

View File

@ -544,7 +544,7 @@ class OpenStackAmuletUtils(AmuletUtils):
return ep return ep
def get_default_keystone_session(self, keystone_sentry, def get_default_keystone_session(self, keystone_sentry,
openstack_release=None): openstack_release=None, api_version=2):
"""Return a keystone session object and client object assuming standard """Return a keystone session object and client object assuming standard
default settings default settings
@ -559,12 +559,12 @@ class OpenStackAmuletUtils(AmuletUtils):
eyc eyc
""" """
self.log.debug('Authenticating keystone admin...') self.log.debug('Authenticating keystone admin...')
api_version = 2
client_class = keystone_client.Client
# 11 => xenial_queens # 11 => xenial_queens
if openstack_release and openstack_release >= 11: if api_version == 3 or (openstack_release and openstack_release >= 11):
api_version = 3
client_class = keystone_client_v3.Client client_class = keystone_client_v3.Client
api_version = 3
else:
client_class = keystone_client.Client
keystone_ip = keystone_sentry.info['public-address'] keystone_ip = keystone_sentry.info['public-address']
session, auth = self.get_keystone_session( session, auth = self.get_keystone_session(
keystone_ip, keystone_ip,

View File

@ -972,6 +972,13 @@ def application_version_set(version):
log("Application Version: {}".format(version)) log("Application Version: {}".format(version))
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
def goal_state():
"""Juju goal state values"""
cmd = ['goal-state', '--format=json']
return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
@translate_exc(from_exc=OSError, to_exc=NotImplementedError) @translate_exc(from_exc=OSError, to_exc=NotImplementedError)
def is_leader(): def is_leader():
"""Does the current unit hold the juju leadership """Does the current unit hold the juju leadership