Enable Bionic as a gate test
Change bionic test from dev to gate for 18.05. Change-Id: I19463ee5e0f555a2a5dda792e6148848b4c7c5f2
This commit is contained in:
parent
18d0a891db
commit
d68abf602d
@ -65,7 +65,8 @@ def get_ca_cert():
|
|||||||
if ca_cert is None:
|
if ca_cert is None:
|
||||||
log("Inspecting identity-service relations for CA SSL certificate.",
|
log("Inspecting identity-service relations for CA SSL certificate.",
|
||||||
level=INFO)
|
level=INFO)
|
||||||
for r_id in relation_ids('identity-service'):
|
for r_id in (relation_ids('identity-service') +
|
||||||
|
relation_ids('identity-credentials')):
|
||||||
for unit in relation_list(r_id):
|
for unit in relation_list(r_id):
|
||||||
if ca_cert is None:
|
if ca_cert is None:
|
||||||
ca_cert = relation_get('ca_cert',
|
ca_cert = relation_get('ca_cert',
|
||||||
@ -76,7 +77,7 @@ def get_ca_cert():
|
|||||||
def retrieve_ca_cert(cert_file):
|
def retrieve_ca_cert(cert_file):
|
||||||
cert = None
|
cert = None
|
||||||
if os.path.isfile(cert_file):
|
if os.path.isfile(cert_file):
|
||||||
with open(cert_file, 'r') as crt:
|
with open(cert_file, 'rb') as crt:
|
||||||
cert = crt.read()
|
cert = crt.read()
|
||||||
return cert
|
return cert
|
||||||
|
|
||||||
|
@ -371,6 +371,7 @@ def distributed_wait(modulo=None, wait=None, operation_name='operation'):
|
|||||||
''' Distribute operations by waiting based on modulo_distribution
|
''' Distribute operations by waiting based on modulo_distribution
|
||||||
|
|
||||||
If modulo and or wait are not set, check config_get for those values.
|
If modulo and or wait are not set, check config_get for those values.
|
||||||
|
If config values are not set, default to modulo=3 and wait=30.
|
||||||
|
|
||||||
:param modulo: int The modulo number creates the group distribution
|
:param modulo: int The modulo number creates the group distribution
|
||||||
:param wait: int The constant time wait value
|
:param wait: int The constant time wait value
|
||||||
@ -382,10 +383,17 @@ def distributed_wait(modulo=None, wait=None, operation_name='operation'):
|
|||||||
:side effect: Calls time.sleep()
|
:side effect: Calls time.sleep()
|
||||||
'''
|
'''
|
||||||
if modulo is None:
|
if modulo is None:
|
||||||
modulo = config_get('modulo-nodes')
|
modulo = config_get('modulo-nodes') or 3
|
||||||
if wait is None:
|
if wait is None:
|
||||||
wait = config_get('known-wait')
|
wait = config_get('known-wait') or 30
|
||||||
calculated_wait = modulo_distribution(modulo=modulo, wait=wait)
|
if juju_is_leader():
|
||||||
|
# The leader should never wait
|
||||||
|
calculated_wait = 0
|
||||||
|
else:
|
||||||
|
# non_zero_wait=True guarantees the non-leader who gets modulo 0
|
||||||
|
# will still wait
|
||||||
|
calculated_wait = modulo_distribution(modulo=modulo, wait=wait,
|
||||||
|
non_zero_wait=True)
|
||||||
msg = "Waiting {} seconds for {} ...".format(calculated_wait,
|
msg = "Waiting {} seconds for {} ...".format(calculated_wait,
|
||||||
operation_name)
|
operation_name)
|
||||||
log(msg, DEBUG)
|
log(msg, DEBUG)
|
||||||
|
@ -441,7 +441,7 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
if rel.get('api_version') != str(api_version):
|
if rel.get('api_version') != str(api_version):
|
||||||
raise Exception("api_version not propagated through relation"
|
raise Exception("api_version not propagated through relation"
|
||||||
" data yet ('{}' != '{}')."
|
" data yet ('{}' != '{}')."
|
||||||
"".format(rel['api_version'], api_version))
|
"".format(rel.get('api_version'), api_version))
|
||||||
|
|
||||||
def keystone_configure_api_version(self, sentry_relation_pairs, deployment,
|
def keystone_configure_api_version(self, sentry_relation_pairs, deployment,
|
||||||
api_version):
|
api_version):
|
||||||
@ -463,16 +463,13 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
deployment._auto_wait_for_status()
|
deployment._auto_wait_for_status()
|
||||||
self.keystone_wait_for_propagation(sentry_relation_pairs, api_version)
|
self.keystone_wait_for_propagation(sentry_relation_pairs, api_version)
|
||||||
|
|
||||||
def authenticate_cinder_admin(self, keystone_sentry, username,
|
def authenticate_cinder_admin(self, keystone, api_version=2):
|
||||||
password, tenant, api_version=2):
|
|
||||||
"""Authenticates admin user with cinder."""
|
"""Authenticates admin user with cinder."""
|
||||||
# NOTE(beisner): cinder python client doesn't accept tokens.
|
self.log.debug('Authenticating cinder admin...')
|
||||||
keystone_ip = keystone_sentry.info['public-address']
|
|
||||||
ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8'))
|
|
||||||
_clients = {
|
_clients = {
|
||||||
1: cinder_client.Client,
|
1: cinder_client.Client,
|
||||||
2: cinder_clientv2.Client}
|
2: cinder_clientv2.Client}
|
||||||
return _clients[api_version](username, password, tenant, ept)
|
return _clients[api_version](session=keystone.session)
|
||||||
|
|
||||||
def authenticate_keystone(self, keystone_ip, username, password,
|
def authenticate_keystone(self, keystone_ip, username, password,
|
||||||
api_version=False, admin_port=False,
|
api_version=False, admin_port=False,
|
||||||
|
@ -797,9 +797,9 @@ class ApacheSSLContext(OSContextGenerator):
|
|||||||
key_filename = 'key'
|
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))
|
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))
|
content=b64decode(key), perms=0o640)
|
||||||
|
|
||||||
def configure_ca(self):
|
def configure_ca(self):
|
||||||
ca_cert = get_ca_cert()
|
ca_cert = get_ca_cert()
|
||||||
@ -1873,10 +1873,11 @@ class EnsureDirContext(OSContextGenerator):
|
|||||||
context is needed to do that before rendering a template.
|
context is needed to do that before rendering a template.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, dirname):
|
def __init__(self, dirname, **kwargs):
|
||||||
'''Used merely to ensure that a given directory exists.'''
|
'''Used merely to ensure that a given directory exists.'''
|
||||||
self.dirname = dirname
|
self.dirname = dirname
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
mkdir(self.dirname)
|
mkdir(self.dirname, **self.kwargs)
|
||||||
return {}
|
return {}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
[oslo_middleware]
|
||||||
|
|
||||||
|
# Bug #1758675
|
||||||
|
enable_proxy_headers_parsing = true
|
||||||
|
|
@ -5,4 +5,7 @@ transport_url = {{ transport_url }}
|
|||||||
{% if notification_topics -%}
|
{% if notification_topics -%}
|
||||||
topics = {{ notification_topics }}
|
topics = {{ notification_topics }}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
{% if notification_format -%}
|
||||||
|
notification_format = {{ notification_format }}
|
||||||
|
{% endif -%}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
@ -306,7 +306,7 @@ def get_os_codename_install_source(src):
|
|||||||
|
|
||||||
if src.startswith('cloud:'):
|
if src.startswith('cloud:'):
|
||||||
ca_rel = src.split(':')[1]
|
ca_rel = src.split(':')[1]
|
||||||
ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
|
ca_rel = ca_rel.split('-')[1].split('/')[0]
|
||||||
return ca_rel
|
return ca_rel
|
||||||
|
|
||||||
# Best guess match based on deb string provided
|
# Best guess match based on deb string provided
|
||||||
|
126
charmhelpers/contrib/openstack/vaultlocker.py
Normal file
126
charmhelpers/contrib/openstack/vaultlocker.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Copyright 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.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import charmhelpers.contrib.openstack.alternatives as alternatives
|
||||||
|
import charmhelpers.contrib.openstack.context as context
|
||||||
|
|
||||||
|
import charmhelpers.core.hookenv as hookenv
|
||||||
|
import charmhelpers.core.host as host
|
||||||
|
import charmhelpers.core.templating as templating
|
||||||
|
import charmhelpers.core.unitdata as unitdata
|
||||||
|
|
||||||
|
VAULTLOCKER_BACKEND = 'charm-vaultlocker'
|
||||||
|
|
||||||
|
|
||||||
|
class VaultKVContext(context.OSContextGenerator):
|
||||||
|
"""Vault KV context for interaction with vault-kv interfaces"""
|
||||||
|
interfaces = ['secrets-storage']
|
||||||
|
|
||||||
|
def __init__(self, secret_backend=None):
|
||||||
|
super(context.OSContextGenerator, self).__init__()
|
||||||
|
self.secret_backend = (
|
||||||
|
secret_backend or 'charm-{}'.format(hookenv.service_name())
|
||||||
|
)
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
db = unitdata.kv()
|
||||||
|
last_token = db.get('last-token')
|
||||||
|
secret_id = db.get('secret-id')
|
||||||
|
for relation_id in hookenv.relation_ids(self.interfaces[0]):
|
||||||
|
for unit in hookenv.related_units(relation_id):
|
||||||
|
data = hookenv.relation_get(unit=unit,
|
||||||
|
rid=relation_id)
|
||||||
|
vault_url = data.get('vault_url')
|
||||||
|
role_id = data.get('{}_role_id'.format(hookenv.local_unit()))
|
||||||
|
token = data.get('{}_token'.format(hookenv.local_unit()))
|
||||||
|
|
||||||
|
if all([vault_url, role_id, token]):
|
||||||
|
token = json.loads(token)
|
||||||
|
vault_url = json.loads(vault_url)
|
||||||
|
|
||||||
|
# Tokens may change when secret_id's are being
|
||||||
|
# reissued - if so use token to get new secret_id
|
||||||
|
if token != last_token:
|
||||||
|
secret_id = retrieve_secret_id(
|
||||||
|
url=vault_url,
|
||||||
|
token=token
|
||||||
|
)
|
||||||
|
db.set('secret-id', secret_id)
|
||||||
|
db.set('last-token', token)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
ctxt = {
|
||||||
|
'vault_url': vault_url,
|
||||||
|
'role_id': json.loads(role_id),
|
||||||
|
'secret_id': secret_id,
|
||||||
|
'secret_backend': self.secret_backend,
|
||||||
|
}
|
||||||
|
vault_ca = data.get('vault_ca')
|
||||||
|
if vault_ca:
|
||||||
|
ctxt['vault_ca'] = json.loads(vault_ca)
|
||||||
|
self.complete = True
|
||||||
|
return ctxt
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def write_vaultlocker_conf(context, priority=100):
|
||||||
|
"""Write vaultlocker configuration to disk and install alternative
|
||||||
|
|
||||||
|
:param context: Dict of data from vault-kv relation
|
||||||
|
:ptype: context: dict
|
||||||
|
:param priority: Priority of alternative configuration
|
||||||
|
:ptype: priority: int"""
|
||||||
|
charm_vl_path = "/var/lib/charm/{}/vaultlocker.conf".format(
|
||||||
|
hookenv.service_name()
|
||||||
|
)
|
||||||
|
host.mkdir(os.path.dirname(charm_vl_path), perms=0o700)
|
||||||
|
templating.render(source='vaultlocker.conf.j2',
|
||||||
|
target=charm_vl_path,
|
||||||
|
context=context, perms=0o600),
|
||||||
|
alternatives.install_alternative('vaultlocker.conf',
|
||||||
|
'/etc/vaultlocker/vaultlocker.conf',
|
||||||
|
charm_vl_path, priority)
|
||||||
|
|
||||||
|
|
||||||
|
def vault_relation_complete(backend=None):
|
||||||
|
"""Determine whether vault relation is complete
|
||||||
|
|
||||||
|
:param backend: Name of secrets backend requested
|
||||||
|
:ptype backend: string
|
||||||
|
:returns: whether the relation to vault is complete
|
||||||
|
:rtype: bool"""
|
||||||
|
vault_kv = VaultKVContext(secret_backend=backend or VAULTLOCKER_BACKEND)
|
||||||
|
vault_kv()
|
||||||
|
return vault_kv.complete
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: contrib a high level unwrap method to hvac that works
|
||||||
|
def retrieve_secret_id(url, token):
|
||||||
|
"""Retrieve a response-wrapped secret_id from Vault
|
||||||
|
|
||||||
|
:param url: URL to Vault Server
|
||||||
|
:ptype url: str
|
||||||
|
:param token: One shot Token to use
|
||||||
|
:ptype token: str
|
||||||
|
:returns: secret_id to use for Vault Access
|
||||||
|
:rtype: str"""
|
||||||
|
import hvac
|
||||||
|
client = hvac.Client(url=url, token=token)
|
||||||
|
response = client._post('/v1/sys/wrapping/unwrap')
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return data['data']['secret_id']
|
@ -291,7 +291,7 @@ class Pool(object):
|
|||||||
|
|
||||||
class ReplicatedPool(Pool):
|
class ReplicatedPool(Pool):
|
||||||
def __init__(self, service, name, pg_num=None, replicas=2,
|
def __init__(self, service, name, pg_num=None, replicas=2,
|
||||||
percent_data=10.0):
|
percent_data=10.0, app_name=None):
|
||||||
super(ReplicatedPool, self).__init__(service=service, name=name)
|
super(ReplicatedPool, self).__init__(service=service, name=name)
|
||||||
self.replicas = replicas
|
self.replicas = replicas
|
||||||
if pg_num:
|
if pg_num:
|
||||||
@ -301,6 +301,10 @@ class ReplicatedPool(Pool):
|
|||||||
self.pg_num = min(pg_num, max_pgs)
|
self.pg_num = min(pg_num, max_pgs)
|
||||||
else:
|
else:
|
||||||
self.pg_num = self.get_pgs(self.replicas, percent_data)
|
self.pg_num = self.get_pgs(self.replicas, percent_data)
|
||||||
|
if app_name:
|
||||||
|
self.app_name = app_name
|
||||||
|
else:
|
||||||
|
self.app_name = 'unknown'
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
if not pool_exists(self.service, self.name):
|
if not pool_exists(self.service, self.name):
|
||||||
@ -313,6 +317,12 @@ class ReplicatedPool(Pool):
|
|||||||
update_pool(client=self.service,
|
update_pool(client=self.service,
|
||||||
pool=self.name,
|
pool=self.name,
|
||||||
settings={'size': str(self.replicas)})
|
settings={'size': str(self.replicas)})
|
||||||
|
try:
|
||||||
|
set_app_name_for_pool(client=self.service,
|
||||||
|
pool=self.name,
|
||||||
|
name=self.app_name)
|
||||||
|
except CalledProcessError:
|
||||||
|
log('Could not set app name for pool {}'.format(self.name, level=WARNING))
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@ -320,10 +330,14 @@ class ReplicatedPool(Pool):
|
|||||||
# Default jerasure erasure coded pool
|
# Default jerasure erasure coded pool
|
||||||
class ErasurePool(Pool):
|
class ErasurePool(Pool):
|
||||||
def __init__(self, service, name, erasure_code_profile="default",
|
def __init__(self, service, name, erasure_code_profile="default",
|
||||||
percent_data=10.0):
|
percent_data=10.0, app_name=None):
|
||||||
super(ErasurePool, self).__init__(service=service, name=name)
|
super(ErasurePool, self).__init__(service=service, name=name)
|
||||||
self.erasure_code_profile = erasure_code_profile
|
self.erasure_code_profile = erasure_code_profile
|
||||||
self.percent_data = percent_data
|
self.percent_data = percent_data
|
||||||
|
if app_name:
|
||||||
|
self.app_name = app_name
|
||||||
|
else:
|
||||||
|
self.app_name = 'unknown'
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
if not pool_exists(self.service, self.name):
|
if not pool_exists(self.service, self.name):
|
||||||
@ -355,6 +369,12 @@ class ErasurePool(Pool):
|
|||||||
'erasure', self.erasure_code_profile]
|
'erasure', self.erasure_code_profile]
|
||||||
try:
|
try:
|
||||||
check_call(cmd)
|
check_call(cmd)
|
||||||
|
try:
|
||||||
|
set_app_name_for_pool(client=self.service,
|
||||||
|
pool=self.name,
|
||||||
|
name=self.app_name)
|
||||||
|
except CalledProcessError:
|
||||||
|
log('Could not set app name for pool {}'.format(self.name, level=WARNING))
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@ -778,6 +798,25 @@ def update_pool(client, pool, settings):
|
|||||||
check_call(cmd)
|
check_call(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def set_app_name_for_pool(client, pool, name):
|
||||||
|
"""
|
||||||
|
Calls `osd pool application enable` for the specified pool name
|
||||||
|
|
||||||
|
:param client: Name of the ceph client to use
|
||||||
|
:type client: str
|
||||||
|
:param pool: Pool to set app name for
|
||||||
|
:type pool: str
|
||||||
|
:param name: app name for the specified pool
|
||||||
|
:type name: str
|
||||||
|
|
||||||
|
:raises: CalledProcessError if ceph call fails
|
||||||
|
"""
|
||||||
|
if ceph_version() >= '12.0.0':
|
||||||
|
cmd = ['ceph', '--id', client, 'osd', 'pool',
|
||||||
|
'application', 'enable', pool, name]
|
||||||
|
check_call(cmd)
|
||||||
|
|
||||||
|
|
||||||
def create_pool(service, name, replicas=3, pg_num=None):
|
def create_pool(service, name, replicas=3, pg_num=None):
|
||||||
"""Create a new RADOS pool."""
|
"""Create a new RADOS pool."""
|
||||||
if pool_exists(service, name):
|
if pool_exists(service, name):
|
||||||
|
@ -151,3 +151,32 @@ def extend_logical_volume_by_device(lv_name, block_device):
|
|||||||
'''
|
'''
|
||||||
cmd = ['lvextend', lv_name, block_device]
|
cmd = ['lvextend', lv_name, block_device]
|
||||||
check_call(cmd)
|
check_call(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def create_logical_volume(lv_name, volume_group, size=None):
|
||||||
|
'''
|
||||||
|
Create a new logical volume in an existing volume group
|
||||||
|
|
||||||
|
:param lv_name: str: name of logical volume to be created.
|
||||||
|
:param volume_group: str: Name of volume group to use for the new volume.
|
||||||
|
:param size: str: Size of logical volume to create (100% if not supplied)
|
||||||
|
:raises subprocess.CalledProcessError: in the event that the lvcreate fails.
|
||||||
|
'''
|
||||||
|
if size:
|
||||||
|
check_call([
|
||||||
|
'lvcreate',
|
||||||
|
'--yes',
|
||||||
|
'-L',
|
||||||
|
'{}'.format(size),
|
||||||
|
'-n', lv_name, volume_group
|
||||||
|
])
|
||||||
|
# create the lv with all the space available, this is needed because the
|
||||||
|
# system call is different for LVM
|
||||||
|
else:
|
||||||
|
check_call([
|
||||||
|
'lvcreate',
|
||||||
|
'--yes',
|
||||||
|
'-l',
|
||||||
|
'100%FREE',
|
||||||
|
'-n', lv_name, volume_group
|
||||||
|
])
|
||||||
|
@ -67,3 +67,19 @@ def is_device_mounted(device):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
return bool(re.search(r'MOUNTPOINT=".+"', out))
|
return bool(re.search(r'MOUNTPOINT=".+"', out))
|
||||||
|
|
||||||
|
|
||||||
|
def mkfs_xfs(device, force=False):
|
||||||
|
"""Format device with XFS filesystem.
|
||||||
|
|
||||||
|
By default this should fail if the device already has a filesystem on it.
|
||||||
|
:param device: Full path to device to format
|
||||||
|
:ptype device: tr
|
||||||
|
:param force: Force operation
|
||||||
|
:ptype: force: boolean"""
|
||||||
|
cmd = ['mkfs.xfs']
|
||||||
|
if force:
|
||||||
|
cmd.append("-f")
|
||||||
|
|
||||||
|
cmd += ['-i', 'size=1024', device]
|
||||||
|
check_call(cmd)
|
||||||
|
@ -27,6 +27,7 @@ import glob
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import errno
|
import errno
|
||||||
@ -67,7 +68,7 @@ def cached(func):
|
|||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
global cache
|
global cache
|
||||||
key = str((func, args, kwargs))
|
key = json.dumps((func, args, kwargs), sort_keys=True, default=str)
|
||||||
try:
|
try:
|
||||||
return cache[key]
|
return cache[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -289,7 +290,7 @@ class Config(dict):
|
|||||||
self.implicit_save = True
|
self.implicit_save = True
|
||||||
self._prev_dict = None
|
self._prev_dict = None
|
||||||
self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
|
self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
|
||||||
if os.path.exists(self.path):
|
if os.path.exists(self.path) and os.stat(self.path).st_size:
|
||||||
self.load_previous()
|
self.load_previous()
|
||||||
atexit(self._implicit_save)
|
atexit(self._implicit_save)
|
||||||
|
|
||||||
@ -309,7 +310,11 @@ class Config(dict):
|
|||||||
"""
|
"""
|
||||||
self.path = path or self.path
|
self.path = path or self.path
|
||||||
with open(self.path) as f:
|
with open(self.path) as f:
|
||||||
|
try:
|
||||||
self._prev_dict = json.load(f)
|
self._prev_dict = json.load(f)
|
||||||
|
except ValueError as e:
|
||||||
|
log('Unable to parse previous config data - {}'.format(str(e)),
|
||||||
|
level=ERROR)
|
||||||
for k, v in copy.deepcopy(self._prev_dict).items():
|
for k, v in copy.deepcopy(self._prev_dict).items():
|
||||||
if k not in self:
|
if k not in self:
|
||||||
self[k] = v
|
self[k] = v
|
||||||
@ -353,22 +358,40 @@ class Config(dict):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
@cached
|
_cache_config = None
|
||||||
|
|
||||||
|
|
||||||
def config(scope=None):
|
def config(scope=None):
|
||||||
"""Juju charm configuration"""
|
"""
|
||||||
config_cmd_line = ['config-get']
|
Get the juju charm configuration (scope==None) or individual key,
|
||||||
if scope is not None:
|
(scope=str). The returned value is a Python data structure loaded as
|
||||||
config_cmd_line.append(scope)
|
JSON from the Juju config command.
|
||||||
else:
|
|
||||||
config_cmd_line.append('--all')
|
:param scope: If set, return the value for the specified key.
|
||||||
config_cmd_line.append('--format=json')
|
:type scope: Optional[str]
|
||||||
|
:returns: Either the whole config as a Config, or a key from it.
|
||||||
|
:rtype: Any
|
||||||
|
"""
|
||||||
|
global _cache_config
|
||||||
|
config_cmd_line = ['config-get', '--all', '--format=json']
|
||||||
try:
|
try:
|
||||||
|
# JSON Decode Exception for Python3.5+
|
||||||
|
exc_json = json.decoder.JSONDecodeError
|
||||||
|
except AttributeError:
|
||||||
|
# JSON Decode Exception for Python2.7 through Python3.4
|
||||||
|
exc_json = ValueError
|
||||||
|
try:
|
||||||
|
if _cache_config is None:
|
||||||
config_data = json.loads(
|
config_data = json.loads(
|
||||||
subprocess.check_output(config_cmd_line).decode('UTF-8'))
|
subprocess.check_output(config_cmd_line).decode('UTF-8'))
|
||||||
|
_cache_config = Config(config_data)
|
||||||
if scope is not None:
|
if scope is not None:
|
||||||
return config_data
|
return _cache_config.get(scope)
|
||||||
return Config(config_data)
|
return _cache_config
|
||||||
except ValueError:
|
except (exc_json, UnicodeDecodeError) as e:
|
||||||
|
log('Unable to parse output from config-get: config_cmd_line="{}" '
|
||||||
|
'message="{}"'
|
||||||
|
.format(config_cmd_line, str(e)), level=ERROR)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -1043,7 +1066,6 @@ def juju_version():
|
|||||||
universal_newlines=True).strip()
|
universal_newlines=True).strip()
|
||||||
|
|
||||||
|
|
||||||
@cached
|
|
||||||
def has_juju_version(minimum_version):
|
def has_juju_version(minimum_version):
|
||||||
"""Return True if the Juju version is at least the provided version"""
|
"""Return True if the Juju version is at least the provided version"""
|
||||||
return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
|
return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
|
||||||
@ -1103,6 +1125,8 @@ def _run_atexit():
|
|||||||
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
|
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
|
||||||
def network_get_primary_address(binding):
|
def network_get_primary_address(binding):
|
||||||
'''
|
'''
|
||||||
|
Deprecated since Juju 2.3; use network_get()
|
||||||
|
|
||||||
Retrieve the primary network address for a named binding
|
Retrieve the primary network address for a named binding
|
||||||
|
|
||||||
:param binding: string. The name of a relation of extra-binding
|
:param binding: string. The name of a relation of extra-binding
|
||||||
@ -1123,7 +1147,6 @@ def network_get_primary_address(binding):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
|
|
||||||
def network_get(endpoint, relation_id=None):
|
def network_get(endpoint, relation_id=None):
|
||||||
"""
|
"""
|
||||||
Retrieve the network details for a relation endpoint
|
Retrieve the network details for a relation endpoint
|
||||||
@ -1131,24 +1154,20 @@ def network_get(endpoint, relation_id=None):
|
|||||||
:param endpoint: string. The name of a relation endpoint
|
:param endpoint: string. The name of a relation endpoint
|
||||||
:param relation_id: int. The ID of the relation for the current context.
|
:param relation_id: int. The ID of the relation for the current context.
|
||||||
:return: dict. The loaded YAML output of the network-get query.
|
:return: dict. The loaded YAML output of the network-get query.
|
||||||
:raise: NotImplementedError if run on Juju < 2.1
|
:raise: NotImplementedError if request not supported by the Juju version.
|
||||||
"""
|
"""
|
||||||
|
if not has_juju_version('2.2'):
|
||||||
|
raise NotImplementedError(juju_version()) # earlier versions require --primary-address
|
||||||
|
if relation_id and not has_juju_version('2.3'):
|
||||||
|
raise NotImplementedError # 2.3 added the -r option
|
||||||
|
|
||||||
cmd = ['network-get', endpoint, '--format', 'yaml']
|
cmd = ['network-get', endpoint, '--format', 'yaml']
|
||||||
if relation_id:
|
if relation_id:
|
||||||
cmd.append('-r')
|
cmd.append('-r')
|
||||||
cmd.append(relation_id)
|
cmd.append(relation_id)
|
||||||
try:
|
|
||||||
response = subprocess.check_output(
|
response = subprocess.check_output(
|
||||||
cmd,
|
cmd,
|
||||||
stderr=subprocess.STDOUT).decode('UTF-8').strip()
|
stderr=subprocess.STDOUT).decode('UTF-8').strip()
|
||||||
except CalledProcessError as e:
|
|
||||||
# Early versions of Juju 2.0.x required the --primary-address argument.
|
|
||||||
# We catch that condition here and raise NotImplementedError since
|
|
||||||
# the requested semantics are not available - the caller can then
|
|
||||||
# use the network_get_primary_address() method instead.
|
|
||||||
if '--primary-address is currently required' in e.output.decode('UTF-8'):
|
|
||||||
raise NotImplementedError
|
|
||||||
raise
|
|
||||||
return yaml.safe_load(response)
|
return yaml.safe_load(response)
|
||||||
|
|
||||||
|
|
||||||
@ -1204,9 +1223,23 @@ def iter_units_for_relation_name(relation_name):
|
|||||||
|
|
||||||
def ingress_address(rid=None, unit=None):
|
def ingress_address(rid=None, unit=None):
|
||||||
"""
|
"""
|
||||||
Retrieve the ingress-address from a relation when available. Otherwise,
|
Retrieve the ingress-address from a relation when available.
|
||||||
return the private-address. This function is to be used on the consuming
|
Otherwise, return the private-address.
|
||||||
side of the relation.
|
|
||||||
|
When used on the consuming side of the relation (unit is a remote
|
||||||
|
unit), the ingress-address is the IP address that this unit needs
|
||||||
|
to use to reach the provided service on the remote unit.
|
||||||
|
|
||||||
|
When used on the providing side of the relation (unit == local_unit()),
|
||||||
|
the ingress-address is the IP address that is advertised to remote
|
||||||
|
units on this relation. Remote units need to use this address to
|
||||||
|
reach the local provided service on this unit.
|
||||||
|
|
||||||
|
Note that charms may document some other method to use in
|
||||||
|
preference to the ingress_address(), such as an address provided
|
||||||
|
on a different relation attribute or a service discovery mechanism.
|
||||||
|
This allows charms to redirect inbound connections to their peers
|
||||||
|
or different applications such as load balancers.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
addresses = [ingress_address(rid=u.rid, unit=u.unit)
|
addresses = [ingress_address(rid=u.rid, unit=u.unit)
|
||||||
@ -1220,3 +1253,40 @@ def ingress_address(rid=None, unit=None):
|
|||||||
settings = relation_get(rid=rid, unit=unit)
|
settings = relation_get(rid=rid, unit=unit)
|
||||||
return (settings.get('ingress-address') or
|
return (settings.get('ingress-address') or
|
||||||
settings.get('private-address'))
|
settings.get('private-address'))
|
||||||
|
|
||||||
|
|
||||||
|
def egress_subnets(rid=None, unit=None):
|
||||||
|
"""
|
||||||
|
Retrieve the egress-subnets from a relation.
|
||||||
|
|
||||||
|
This function is to be used on the providing side of the
|
||||||
|
relation, and provides the ranges of addresses that client
|
||||||
|
connections may come from. The result is uninteresting on
|
||||||
|
the consuming side of a relation (unit == local_unit()).
|
||||||
|
|
||||||
|
Returns a stable list of subnets in CIDR format.
|
||||||
|
eg. ['192.168.1.0/24', '2001::F00F/128']
|
||||||
|
|
||||||
|
If egress-subnets is not available, falls back to using the published
|
||||||
|
ingress-address, or finally private-address.
|
||||||
|
|
||||||
|
:param rid: string relation id
|
||||||
|
:param unit: string unit name
|
||||||
|
:side effect: calls relation_get
|
||||||
|
:return: list of subnets in CIDR format. eg. ['192.168.1.0/24', '2001::F00F/128']
|
||||||
|
"""
|
||||||
|
def _to_range(addr):
|
||||||
|
if re.search(r'^(?:\d{1,3}\.){3}\d{1,3}$', addr) is not None:
|
||||||
|
addr += '/32'
|
||||||
|
elif ':' in addr and '/' not in addr: # IPv6
|
||||||
|
addr += '/128'
|
||||||
|
return addr
|
||||||
|
|
||||||
|
settings = relation_get(rid=rid, unit=unit)
|
||||||
|
if 'egress-subnets' in settings:
|
||||||
|
return [n.strip() for n in settings['egress-subnets'].split(',') if n.strip()]
|
||||||
|
if 'ingress-address' in settings:
|
||||||
|
return [_to_range(settings['ingress-address'])]
|
||||||
|
if 'private-address' in settings:
|
||||||
|
return [_to_range(settings['private-address'])]
|
||||||
|
return [] # Should never happen
|
||||||
|
@ -993,7 +993,7 @@ def updatedb(updatedb_text, new_path):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def modulo_distribution(modulo=3, wait=30):
|
def modulo_distribution(modulo=3, wait=30, non_zero_wait=False):
|
||||||
""" Modulo distribution
|
""" Modulo distribution
|
||||||
|
|
||||||
This helper uses the unit number, a modulo value and a constant wait time
|
This helper uses the unit number, a modulo value and a constant wait time
|
||||||
@ -1015,7 +1015,14 @@ def modulo_distribution(modulo=3, wait=30):
|
|||||||
|
|
||||||
@param modulo: int The modulo number creates the group distribution
|
@param modulo: int The modulo number creates the group distribution
|
||||||
@param wait: int The constant time wait value
|
@param wait: int The constant time wait value
|
||||||
|
@param non_zero_wait: boolean Override unit % modulo == 0,
|
||||||
|
return modulo * wait. Used to avoid collisions with
|
||||||
|
leader nodes which are often given priority.
|
||||||
@return: int Calculated time to wait for unit operation
|
@return: int Calculated time to wait for unit operation
|
||||||
"""
|
"""
|
||||||
unit_number = int(local_unit().split('/')[1])
|
unit_number = int(local_unit().split('/')[1])
|
||||||
return (unit_number % modulo) * wait
|
calculated_wait_time = (unit_number % modulo) * wait
|
||||||
|
if non_zero_wait and calculated_wait_time == 0:
|
||||||
|
return modulo * wait
|
||||||
|
else:
|
||||||
|
return calculated_wait_time
|
||||||
|
@ -307,23 +307,34 @@ class PortManagerCallback(ManagerCallback):
|
|||||||
"""
|
"""
|
||||||
def __call__(self, manager, service_name, event_name):
|
def __call__(self, manager, service_name, event_name):
|
||||||
service = manager.get_service(service_name)
|
service = manager.get_service(service_name)
|
||||||
new_ports = service.get('ports', [])
|
# turn this generator into a list,
|
||||||
|
# as we'll be going over it multiple times
|
||||||
|
new_ports = list(service.get('ports', []))
|
||||||
port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
|
port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
|
||||||
if os.path.exists(port_file):
|
if os.path.exists(port_file):
|
||||||
with open(port_file) as fp:
|
with open(port_file) as fp:
|
||||||
old_ports = fp.read().split(',')
|
old_ports = fp.read().split(',')
|
||||||
for old_port in old_ports:
|
for old_port in old_ports:
|
||||||
if bool(old_port):
|
if bool(old_port) and not self.ports_contains(old_port, new_ports):
|
||||||
old_port = int(old_port)
|
|
||||||
if old_port not in new_ports:
|
|
||||||
hookenv.close_port(old_port)
|
hookenv.close_port(old_port)
|
||||||
with open(port_file, 'w') as fp:
|
with open(port_file, 'w') as fp:
|
||||||
fp.write(','.join(str(port) for port in new_ports))
|
fp.write(','.join(str(port) for port in new_ports))
|
||||||
for port in new_ports:
|
for port in new_ports:
|
||||||
|
# A port is either a number or 'ICMP'
|
||||||
|
protocol = 'TCP'
|
||||||
|
if str(port).upper() == 'ICMP':
|
||||||
|
protocol = 'ICMP'
|
||||||
if event_name == 'start':
|
if event_name == 'start':
|
||||||
hookenv.open_port(port)
|
hookenv.open_port(port, protocol)
|
||||||
elif event_name == 'stop':
|
elif event_name == 'stop':
|
||||||
hookenv.close_port(port)
|
hookenv.close_port(port, protocol)
|
||||||
|
|
||||||
|
def ports_contains(self, port, ports):
|
||||||
|
if not bool(port):
|
||||||
|
return False
|
||||||
|
if str(port).upper() != 'ICMP':
|
||||||
|
port = int(port)
|
||||||
|
return port in ports
|
||||||
|
|
||||||
|
|
||||||
def service_stop(service_name):
|
def service_stop(service_name):
|
||||||
|
@ -31,18 +31,22 @@ __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
|
|||||||
def create(sysctl_dict, sysctl_file):
|
def create(sysctl_dict, sysctl_file):
|
||||||
"""Creates a sysctl.conf file from a YAML associative array
|
"""Creates a sysctl.conf file from a YAML associative array
|
||||||
|
|
||||||
:param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
|
:param sysctl_dict: a dict or YAML-formatted string of sysctl
|
||||||
|
options eg "{ 'kernel.max_pid': 1337 }"
|
||||||
:type sysctl_dict: str
|
:type sysctl_dict: str
|
||||||
:param sysctl_file: path to the sysctl file to be saved
|
:param sysctl_file: path to the sysctl file to be saved
|
||||||
:type sysctl_file: str or unicode
|
:type sysctl_file: str or unicode
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
|
if type(sysctl_dict) is not dict:
|
||||||
try:
|
try:
|
||||||
sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
|
sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
|
log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
|
||||||
level=ERROR)
|
level=ERROR)
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
sysctl_dict_parsed = sysctl_dict
|
||||||
|
|
||||||
with open(sysctl_file, "w") as fd:
|
with open(sysctl_file, "w") as fd:
|
||||||
for key, value in sysctl_dict_parsed.items():
|
for key, value in sysctl_dict_parsed.items():
|
||||||
|
@ -166,6 +166,10 @@ class Storage(object):
|
|||||||
|
|
||||||
To support dicts, lists, integer, floats, and booleans values
|
To support dicts, lists, integer, floats, and booleans values
|
||||||
are automatically json encoded/decoded.
|
are automatically json encoded/decoded.
|
||||||
|
|
||||||
|
Note: to facilitate unit testing, ':memory:' can be passed as the
|
||||||
|
path parameter which causes sqlite3 to only build the db in memory.
|
||||||
|
This should only be used for testing purposes.
|
||||||
"""
|
"""
|
||||||
def __init__(self, path=None):
|
def __init__(self, path=None):
|
||||||
self.db_path = path
|
self.db_path = path
|
||||||
@ -175,6 +179,7 @@ class Storage(object):
|
|||||||
else:
|
else:
|
||||||
self.db_path = os.path.join(
|
self.db_path = os.path.join(
|
||||||
os.environ.get('CHARM_DIR', ''), '.unit-state.db')
|
os.environ.get('CHARM_DIR', ''), '.unit-state.db')
|
||||||
|
if self.db_path != ':memory:':
|
||||||
with open(self.db_path, 'a') as f:
|
with open(self.db_path, 'a') as f:
|
||||||
os.fchmod(f.fileno(), 0o600)
|
os.fchmod(f.fileno(), 0o600)
|
||||||
self.conn = sqlite3.connect('%s' % self.db_path)
|
self.conn = sqlite3.connect('%s' % self.db_path)
|
||||||
|
@ -44,6 +44,7 @@ ARCH_TO_PROPOSED_POCKET = {
|
|||||||
'x86_64': PROPOSED_POCKET,
|
'x86_64': PROPOSED_POCKET,
|
||||||
'ppc64le': PROPOSED_PORTS_POCKET,
|
'ppc64le': PROPOSED_PORTS_POCKET,
|
||||||
'aarch64': PROPOSED_PORTS_POCKET,
|
'aarch64': PROPOSED_PORTS_POCKET,
|
||||||
|
's390x': PROPOSED_PORTS_POCKET,
|
||||||
}
|
}
|
||||||
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
|
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
|
||||||
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
|
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
|
||||||
|
@ -441,7 +441,7 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
if rel.get('api_version') != str(api_version):
|
if rel.get('api_version') != str(api_version):
|
||||||
raise Exception("api_version not propagated through relation"
|
raise Exception("api_version not propagated through relation"
|
||||||
" data yet ('{}' != '{}')."
|
" data yet ('{}' != '{}')."
|
||||||
"".format(rel['api_version'], api_version))
|
"".format(rel.get('api_version'), api_version))
|
||||||
|
|
||||||
def keystone_configure_api_version(self, sentry_relation_pairs, deployment,
|
def keystone_configure_api_version(self, sentry_relation_pairs, deployment,
|
||||||
api_version):
|
api_version):
|
||||||
@ -463,16 +463,13 @@ class OpenStackAmuletUtils(AmuletUtils):
|
|||||||
deployment._auto_wait_for_status()
|
deployment._auto_wait_for_status()
|
||||||
self.keystone_wait_for_propagation(sentry_relation_pairs, api_version)
|
self.keystone_wait_for_propagation(sentry_relation_pairs, api_version)
|
||||||
|
|
||||||
def authenticate_cinder_admin(self, keystone_sentry, username,
|
def authenticate_cinder_admin(self, keystone, api_version=2):
|
||||||
password, tenant, api_version=2):
|
|
||||||
"""Authenticates admin user with cinder."""
|
"""Authenticates admin user with cinder."""
|
||||||
# NOTE(beisner): cinder python client doesn't accept tokens.
|
self.log.debug('Authenticating cinder admin...')
|
||||||
keystone_ip = keystone_sentry.info['public-address']
|
|
||||||
ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8'))
|
|
||||||
_clients = {
|
_clients = {
|
||||||
1: cinder_client.Client,
|
1: cinder_client.Client,
|
||||||
2: cinder_clientv2.Client}
|
2: cinder_clientv2.Client}
|
||||||
return _clients[api_version](username, password, tenant, ept)
|
return _clients[api_version](session=keystone.session)
|
||||||
|
|
||||||
def authenticate_keystone(self, keystone_ip, username, password,
|
def authenticate_keystone(self, keystone_ip, username, password,
|
||||||
api_version=False, admin_port=False,
|
api_version=False, admin_port=False,
|
||||||
|
@ -27,6 +27,7 @@ import glob
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import errno
|
import errno
|
||||||
@ -67,7 +68,7 @@ def cached(func):
|
|||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
global cache
|
global cache
|
||||||
key = str((func, args, kwargs))
|
key = json.dumps((func, args, kwargs), sort_keys=True, default=str)
|
||||||
try:
|
try:
|
||||||
return cache[key]
|
return cache[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -289,7 +290,7 @@ class Config(dict):
|
|||||||
self.implicit_save = True
|
self.implicit_save = True
|
||||||
self._prev_dict = None
|
self._prev_dict = None
|
||||||
self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
|
self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
|
||||||
if os.path.exists(self.path):
|
if os.path.exists(self.path) and os.stat(self.path).st_size:
|
||||||
self.load_previous()
|
self.load_previous()
|
||||||
atexit(self._implicit_save)
|
atexit(self._implicit_save)
|
||||||
|
|
||||||
@ -309,7 +310,11 @@ class Config(dict):
|
|||||||
"""
|
"""
|
||||||
self.path = path or self.path
|
self.path = path or self.path
|
||||||
with open(self.path) as f:
|
with open(self.path) as f:
|
||||||
|
try:
|
||||||
self._prev_dict = json.load(f)
|
self._prev_dict = json.load(f)
|
||||||
|
except ValueError as e:
|
||||||
|
log('Unable to parse previous config data - {}'.format(str(e)),
|
||||||
|
level=ERROR)
|
||||||
for k, v in copy.deepcopy(self._prev_dict).items():
|
for k, v in copy.deepcopy(self._prev_dict).items():
|
||||||
if k not in self:
|
if k not in self:
|
||||||
self[k] = v
|
self[k] = v
|
||||||
@ -353,22 +358,40 @@ class Config(dict):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
@cached
|
_cache_config = None
|
||||||
|
|
||||||
|
|
||||||
def config(scope=None):
|
def config(scope=None):
|
||||||
"""Juju charm configuration"""
|
"""
|
||||||
config_cmd_line = ['config-get']
|
Get the juju charm configuration (scope==None) or individual key,
|
||||||
if scope is not None:
|
(scope=str). The returned value is a Python data structure loaded as
|
||||||
config_cmd_line.append(scope)
|
JSON from the Juju config command.
|
||||||
else:
|
|
||||||
config_cmd_line.append('--all')
|
:param scope: If set, return the value for the specified key.
|
||||||
config_cmd_line.append('--format=json')
|
:type scope: Optional[str]
|
||||||
|
:returns: Either the whole config as a Config, or a key from it.
|
||||||
|
:rtype: Any
|
||||||
|
"""
|
||||||
|
global _cache_config
|
||||||
|
config_cmd_line = ['config-get', '--all', '--format=json']
|
||||||
try:
|
try:
|
||||||
|
# JSON Decode Exception for Python3.5+
|
||||||
|
exc_json = json.decoder.JSONDecodeError
|
||||||
|
except AttributeError:
|
||||||
|
# JSON Decode Exception for Python2.7 through Python3.4
|
||||||
|
exc_json = ValueError
|
||||||
|
try:
|
||||||
|
if _cache_config is None:
|
||||||
config_data = json.loads(
|
config_data = json.loads(
|
||||||
subprocess.check_output(config_cmd_line).decode('UTF-8'))
|
subprocess.check_output(config_cmd_line).decode('UTF-8'))
|
||||||
|
_cache_config = Config(config_data)
|
||||||
if scope is not None:
|
if scope is not None:
|
||||||
return config_data
|
return _cache_config.get(scope)
|
||||||
return Config(config_data)
|
return _cache_config
|
||||||
except ValueError:
|
except (exc_json, UnicodeDecodeError) as e:
|
||||||
|
log('Unable to parse output from config-get: config_cmd_line="{}" '
|
||||||
|
'message="{}"'
|
||||||
|
.format(config_cmd_line, str(e)), level=ERROR)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -1043,7 +1066,6 @@ def juju_version():
|
|||||||
universal_newlines=True).strip()
|
universal_newlines=True).strip()
|
||||||
|
|
||||||
|
|
||||||
@cached
|
|
||||||
def has_juju_version(minimum_version):
|
def has_juju_version(minimum_version):
|
||||||
"""Return True if the Juju version is at least the provided version"""
|
"""Return True if the Juju version is at least the provided version"""
|
||||||
return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
|
return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
|
||||||
@ -1103,6 +1125,8 @@ def _run_atexit():
|
|||||||
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
|
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
|
||||||
def network_get_primary_address(binding):
|
def network_get_primary_address(binding):
|
||||||
'''
|
'''
|
||||||
|
Deprecated since Juju 2.3; use network_get()
|
||||||
|
|
||||||
Retrieve the primary network address for a named binding
|
Retrieve the primary network address for a named binding
|
||||||
|
|
||||||
:param binding: string. The name of a relation of extra-binding
|
:param binding: string. The name of a relation of extra-binding
|
||||||
@ -1123,7 +1147,6 @@ def network_get_primary_address(binding):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
|
|
||||||
def network_get(endpoint, relation_id=None):
|
def network_get(endpoint, relation_id=None):
|
||||||
"""
|
"""
|
||||||
Retrieve the network details for a relation endpoint
|
Retrieve the network details for a relation endpoint
|
||||||
@ -1131,24 +1154,20 @@ def network_get(endpoint, relation_id=None):
|
|||||||
:param endpoint: string. The name of a relation endpoint
|
:param endpoint: string. The name of a relation endpoint
|
||||||
:param relation_id: int. The ID of the relation for the current context.
|
:param relation_id: int. The ID of the relation for the current context.
|
||||||
:return: dict. The loaded YAML output of the network-get query.
|
:return: dict. The loaded YAML output of the network-get query.
|
||||||
:raise: NotImplementedError if run on Juju < 2.1
|
:raise: NotImplementedError if request not supported by the Juju version.
|
||||||
"""
|
"""
|
||||||
|
if not has_juju_version('2.2'):
|
||||||
|
raise NotImplementedError(juju_version()) # earlier versions require --primary-address
|
||||||
|
if relation_id and not has_juju_version('2.3'):
|
||||||
|
raise NotImplementedError # 2.3 added the -r option
|
||||||
|
|
||||||
cmd = ['network-get', endpoint, '--format', 'yaml']
|
cmd = ['network-get', endpoint, '--format', 'yaml']
|
||||||
if relation_id:
|
if relation_id:
|
||||||
cmd.append('-r')
|
cmd.append('-r')
|
||||||
cmd.append(relation_id)
|
cmd.append(relation_id)
|
||||||
try:
|
|
||||||
response = subprocess.check_output(
|
response = subprocess.check_output(
|
||||||
cmd,
|
cmd,
|
||||||
stderr=subprocess.STDOUT).decode('UTF-8').strip()
|
stderr=subprocess.STDOUT).decode('UTF-8').strip()
|
||||||
except CalledProcessError as e:
|
|
||||||
# Early versions of Juju 2.0.x required the --primary-address argument.
|
|
||||||
# We catch that condition here and raise NotImplementedError since
|
|
||||||
# the requested semantics are not available - the caller can then
|
|
||||||
# use the network_get_primary_address() method instead.
|
|
||||||
if '--primary-address is currently required' in e.output.decode('UTF-8'):
|
|
||||||
raise NotImplementedError
|
|
||||||
raise
|
|
||||||
return yaml.safe_load(response)
|
return yaml.safe_load(response)
|
||||||
|
|
||||||
|
|
||||||
@ -1204,9 +1223,23 @@ def iter_units_for_relation_name(relation_name):
|
|||||||
|
|
||||||
def ingress_address(rid=None, unit=None):
|
def ingress_address(rid=None, unit=None):
|
||||||
"""
|
"""
|
||||||
Retrieve the ingress-address from a relation when available. Otherwise,
|
Retrieve the ingress-address from a relation when available.
|
||||||
return the private-address. This function is to be used on the consuming
|
Otherwise, return the private-address.
|
||||||
side of the relation.
|
|
||||||
|
When used on the consuming side of the relation (unit is a remote
|
||||||
|
unit), the ingress-address is the IP address that this unit needs
|
||||||
|
to use to reach the provided service on the remote unit.
|
||||||
|
|
||||||
|
When used on the providing side of the relation (unit == local_unit()),
|
||||||
|
the ingress-address is the IP address that is advertised to remote
|
||||||
|
units on this relation. Remote units need to use this address to
|
||||||
|
reach the local provided service on this unit.
|
||||||
|
|
||||||
|
Note that charms may document some other method to use in
|
||||||
|
preference to the ingress_address(), such as an address provided
|
||||||
|
on a different relation attribute or a service discovery mechanism.
|
||||||
|
This allows charms to redirect inbound connections to their peers
|
||||||
|
or different applications such as load balancers.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
addresses = [ingress_address(rid=u.rid, unit=u.unit)
|
addresses = [ingress_address(rid=u.rid, unit=u.unit)
|
||||||
@ -1220,3 +1253,40 @@ def ingress_address(rid=None, unit=None):
|
|||||||
settings = relation_get(rid=rid, unit=unit)
|
settings = relation_get(rid=rid, unit=unit)
|
||||||
return (settings.get('ingress-address') or
|
return (settings.get('ingress-address') or
|
||||||
settings.get('private-address'))
|
settings.get('private-address'))
|
||||||
|
|
||||||
|
|
||||||
|
def egress_subnets(rid=None, unit=None):
|
||||||
|
"""
|
||||||
|
Retrieve the egress-subnets from a relation.
|
||||||
|
|
||||||
|
This function is to be used on the providing side of the
|
||||||
|
relation, and provides the ranges of addresses that client
|
||||||
|
connections may come from. The result is uninteresting on
|
||||||
|
the consuming side of a relation (unit == local_unit()).
|
||||||
|
|
||||||
|
Returns a stable list of subnets in CIDR format.
|
||||||
|
eg. ['192.168.1.0/24', '2001::F00F/128']
|
||||||
|
|
||||||
|
If egress-subnets is not available, falls back to using the published
|
||||||
|
ingress-address, or finally private-address.
|
||||||
|
|
||||||
|
:param rid: string relation id
|
||||||
|
:param unit: string unit name
|
||||||
|
:side effect: calls relation_get
|
||||||
|
:return: list of subnets in CIDR format. eg. ['192.168.1.0/24', '2001::F00F/128']
|
||||||
|
"""
|
||||||
|
def _to_range(addr):
|
||||||
|
if re.search(r'^(?:\d{1,3}\.){3}\d{1,3}$', addr) is not None:
|
||||||
|
addr += '/32'
|
||||||
|
elif ':' in addr and '/' not in addr: # IPv6
|
||||||
|
addr += '/128'
|
||||||
|
return addr
|
||||||
|
|
||||||
|
settings = relation_get(rid=rid, unit=unit)
|
||||||
|
if 'egress-subnets' in settings:
|
||||||
|
return [n.strip() for n in settings['egress-subnets'].split(',') if n.strip()]
|
||||||
|
if 'ingress-address' in settings:
|
||||||
|
return [_to_range(settings['ingress-address'])]
|
||||||
|
if 'private-address' in settings:
|
||||||
|
return [_to_range(settings['private-address'])]
|
||||||
|
return [] # Should never happen
|
||||||
|
@ -993,7 +993,7 @@ def updatedb(updatedb_text, new_path):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def modulo_distribution(modulo=3, wait=30):
|
def modulo_distribution(modulo=3, wait=30, non_zero_wait=False):
|
||||||
""" Modulo distribution
|
""" Modulo distribution
|
||||||
|
|
||||||
This helper uses the unit number, a modulo value and a constant wait time
|
This helper uses the unit number, a modulo value and a constant wait time
|
||||||
@ -1015,7 +1015,14 @@ def modulo_distribution(modulo=3, wait=30):
|
|||||||
|
|
||||||
@param modulo: int The modulo number creates the group distribution
|
@param modulo: int The modulo number creates the group distribution
|
||||||
@param wait: int The constant time wait value
|
@param wait: int The constant time wait value
|
||||||
|
@param non_zero_wait: boolean Override unit % modulo == 0,
|
||||||
|
return modulo * wait. Used to avoid collisions with
|
||||||
|
leader nodes which are often given priority.
|
||||||
@return: int Calculated time to wait for unit operation
|
@return: int Calculated time to wait for unit operation
|
||||||
"""
|
"""
|
||||||
unit_number = int(local_unit().split('/')[1])
|
unit_number = int(local_unit().split('/')[1])
|
||||||
return (unit_number % modulo) * wait
|
calculated_wait_time = (unit_number % modulo) * wait
|
||||||
|
if non_zero_wait and calculated_wait_time == 0:
|
||||||
|
return modulo * wait
|
||||||
|
else:
|
||||||
|
return calculated_wait_time
|
||||||
|
@ -307,23 +307,34 @@ class PortManagerCallback(ManagerCallback):
|
|||||||
"""
|
"""
|
||||||
def __call__(self, manager, service_name, event_name):
|
def __call__(self, manager, service_name, event_name):
|
||||||
service = manager.get_service(service_name)
|
service = manager.get_service(service_name)
|
||||||
new_ports = service.get('ports', [])
|
# turn this generator into a list,
|
||||||
|
# as we'll be going over it multiple times
|
||||||
|
new_ports = list(service.get('ports', []))
|
||||||
port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
|
port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
|
||||||
if os.path.exists(port_file):
|
if os.path.exists(port_file):
|
||||||
with open(port_file) as fp:
|
with open(port_file) as fp:
|
||||||
old_ports = fp.read().split(',')
|
old_ports = fp.read().split(',')
|
||||||
for old_port in old_ports:
|
for old_port in old_ports:
|
||||||
if bool(old_port):
|
if bool(old_port) and not self.ports_contains(old_port, new_ports):
|
||||||
old_port = int(old_port)
|
|
||||||
if old_port not in new_ports:
|
|
||||||
hookenv.close_port(old_port)
|
hookenv.close_port(old_port)
|
||||||
with open(port_file, 'w') as fp:
|
with open(port_file, 'w') as fp:
|
||||||
fp.write(','.join(str(port) for port in new_ports))
|
fp.write(','.join(str(port) for port in new_ports))
|
||||||
for port in new_ports:
|
for port in new_ports:
|
||||||
|
# A port is either a number or 'ICMP'
|
||||||
|
protocol = 'TCP'
|
||||||
|
if str(port).upper() == 'ICMP':
|
||||||
|
protocol = 'ICMP'
|
||||||
if event_name == 'start':
|
if event_name == 'start':
|
||||||
hookenv.open_port(port)
|
hookenv.open_port(port, protocol)
|
||||||
elif event_name == 'stop':
|
elif event_name == 'stop':
|
||||||
hookenv.close_port(port)
|
hookenv.close_port(port, protocol)
|
||||||
|
|
||||||
|
def ports_contains(self, port, ports):
|
||||||
|
if not bool(port):
|
||||||
|
return False
|
||||||
|
if str(port).upper() != 'ICMP':
|
||||||
|
port = int(port)
|
||||||
|
return port in ports
|
||||||
|
|
||||||
|
|
||||||
def service_stop(service_name):
|
def service_stop(service_name):
|
||||||
|
@ -31,18 +31,22 @@ __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
|
|||||||
def create(sysctl_dict, sysctl_file):
|
def create(sysctl_dict, sysctl_file):
|
||||||
"""Creates a sysctl.conf file from a YAML associative array
|
"""Creates a sysctl.conf file from a YAML associative array
|
||||||
|
|
||||||
:param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
|
:param sysctl_dict: a dict or YAML-formatted string of sysctl
|
||||||
|
options eg "{ 'kernel.max_pid': 1337 }"
|
||||||
:type sysctl_dict: str
|
:type sysctl_dict: str
|
||||||
:param sysctl_file: path to the sysctl file to be saved
|
:param sysctl_file: path to the sysctl file to be saved
|
||||||
:type sysctl_file: str or unicode
|
:type sysctl_file: str or unicode
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
|
if type(sysctl_dict) is not dict:
|
||||||
try:
|
try:
|
||||||
sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
|
sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
|
log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
|
||||||
level=ERROR)
|
level=ERROR)
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
sysctl_dict_parsed = sysctl_dict
|
||||||
|
|
||||||
with open(sysctl_file, "w") as fd:
|
with open(sysctl_file, "w") as fd:
|
||||||
for key, value in sysctl_dict_parsed.items():
|
for key, value in sysctl_dict_parsed.items():
|
||||||
|
@ -166,6 +166,10 @@ class Storage(object):
|
|||||||
|
|
||||||
To support dicts, lists, integer, floats, and booleans values
|
To support dicts, lists, integer, floats, and booleans values
|
||||||
are automatically json encoded/decoded.
|
are automatically json encoded/decoded.
|
||||||
|
|
||||||
|
Note: to facilitate unit testing, ':memory:' can be passed as the
|
||||||
|
path parameter which causes sqlite3 to only build the db in memory.
|
||||||
|
This should only be used for testing purposes.
|
||||||
"""
|
"""
|
||||||
def __init__(self, path=None):
|
def __init__(self, path=None):
|
||||||
self.db_path = path
|
self.db_path = path
|
||||||
@ -175,6 +179,7 @@ class Storage(object):
|
|||||||
else:
|
else:
|
||||||
self.db_path = os.path.join(
|
self.db_path = os.path.join(
|
||||||
os.environ.get('CHARM_DIR', ''), '.unit-state.db')
|
os.environ.get('CHARM_DIR', ''), '.unit-state.db')
|
||||||
|
if self.db_path != ':memory:':
|
||||||
with open(self.db_path, 'a') as f:
|
with open(self.db_path, 'a') as f:
|
||||||
os.fchmod(f.fileno(), 0o600)
|
os.fchmod(f.fileno(), 0o600)
|
||||||
self.conn = sqlite3.connect('%s' % self.db_path)
|
self.conn = sqlite3.connect('%s' % self.db_path)
|
||||||
|
2
tox.ini
2
tox.ini
@ -60,7 +60,7 @@ basepython = python2.7
|
|||||||
deps = -r{toxinidir}/requirements.txt
|
deps = -r{toxinidir}/requirements.txt
|
||||||
-r{toxinidir}/test-requirements.txt
|
-r{toxinidir}/test-requirements.txt
|
||||||
commands =
|
commands =
|
||||||
bundletester -vl DEBUG -r json -o func-results.json gate-basic-xenial-pike --no-destroy
|
bundletester -vl DEBUG -r json -o func-results.json gate-basic-bionic-queens --no-destroy
|
||||||
|
|
||||||
[testenv:func27-dfs]
|
[testenv:func27-dfs]
|
||||||
# Charm Functional Test
|
# Charm Functional Test
|
||||||
|
Loading…
Reference in New Issue
Block a user