330
428
def config(scope=None):
331
"""Juju charm configuration"""
332
config_cmd_line = ['config-get']
333
if scope is not None:
334
config_cmd_line.append(scope)
336
config_cmd_line.append('--all')
337
config_cmd_line.append('--format=json')
430
Get the juju charm configuration (scope==None) or individual key,
431
(scope=str). The returned value is a Python data structure loaded as
432
JSON from the Juju config command.
434
:param scope: If set, return the value for the specified key.
435
:type scope: Optional[str]
436
:returns: Either the whole config as a Config, or a key from it.
440
config_cmd_line = ['config-get', '--all', '--format=json']
339
config_data = json.loads(
340
subprocess.check_output(config_cmd_line).decode('UTF-8'))
442
if _cache_config is None:
443
config_data = json.loads(
444
subprocess.check_output(config_cmd_line).decode('UTF-8'))
445
_cache_config = Config(config_data)
341
446
if scope is not None:
343
return Config(config_data)
447
return _cache_config.get(scope)
449
except (json.decoder.JSONDecodeError, UnicodeDecodeError) as e:
450
log('Unable to parse output from config-get: config_cmd_line="{}" '
452
.format(config_cmd_line, str(e)), level=ERROR)
349
def relation_get(attribute=None, unit=None, rid=None):
457
def relation_get(attribute=None, unit=None, rid=None, app=None):
350
458
"""Get relation information"""
351
459
_args = ['relation-get', '--format=json']
462
raise ValueError("Cannot use both 'unit' and 'app'")
463
_args.append('--app')
353
465
_args.append('-r')
354
466
_args.append(rid)
355
467
_args.append(attribute or '-')
468
# unit or application name
470
_args.append(unit or app)
359
472
return json.loads(subprocess.check_output(_args).decode('UTF-8'))
360
473
except ValueError:
435
564
subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
567
def expected_peer_units():
568
"""Get a generator for units we expect to join peer relation based on
571
The local unit is excluded from the result to make it easy to gauge
572
completion of all peers joining the relation with existing hook tools.
575
log('peer {} of {} joined peer relation'
576
.format(len(related_units()),
577
len(list(expected_peer_units()))))
579
This function will raise NotImplementedError if used with juju versions
580
without goal-state support.
583
:rtype: types.GeneratorType
584
:raises: NotImplementedError
586
if not has_juju_version("2.4.0"):
587
# goal-state first appeared in 2.4.0.
588
raise NotImplementedError("goal-state")
589
_goal_state = goal_state()
590
return (key for key in _goal_state['units']
591
if '/' in key and key != local_unit())
594
def expected_related_units(reltype=None):
595
"""Get a generator for units we expect to join relation based on
598
Note that you can not use this function for the peer relation, take a look
599
at expected_peer_units() for that.
601
This function will raise KeyError if you request information for a
602
relation type for which juju goal-state does not have information. It will
603
raise NotImplementedError if used with juju versions without goal-state
607
log('participant {} of {} joined relation {}'
608
.format(len(related_units()),
609
len(list(expected_related_units())),
612
:param reltype: Relation type to list data for, default is to list data for
613
the relation type we are currently executing a hook for.
616
:rtype: types.GeneratorType
617
:raises: KeyError, NotImplementedError
619
if not has_juju_version("2.4.4"):
620
# goal-state existed in 2.4.0, but did not list individual units to
621
# join a relation in 2.4.1 through 2.4.3. (LP: #1794739)
622
raise NotImplementedError("goal-state relation unit count")
623
reltype = reltype or relation_type()
624
_goal_state = goal_state()
625
return (key for key in _goal_state['relations'][reltype] if '/' in key)
439
629
def relation_for_unit(unit=None, rid=None):
440
"""Get the json represenation of a unit's relation"""
630
"""Get the json representation of a unit's relation"""
441
631
unit = unit or remote_unit()
442
632
relation = relation_get(unit=unit, rid=rid)
443
633
for key in relation:
767
1014
return action_data
1018
@deprecate("moved to action_get()", log=log)
1019
def function_get(key=None):
1022
Gets the value of an action parameter, or all key/value param pairs.
1024
cmd = ['function-get']
1025
# Fallback for older charms.
1026
if not cmd_exists('function-get'):
1027
cmd = ['action-get']
1031
cmd.append('--format=json')
1032
function_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1033
return function_data
770
1036
def action_set(values):
771
"""Sets the values to be returned after the action finishes"""
1037
"""Sets the values to be returned after the action finishes."""
772
1038
cmd = ['action-set']
773
1039
for k, v in list(values.items()):
774
1040
cmd.append('{}={}'.format(k, v))
775
1041
subprocess.check_call(cmd)
1044
@deprecate("moved to action_set()", log=log)
1045
def function_set(values):
1048
Sets the values to be returned after the function finishes.
1050
cmd = ['function-set']
1051
# Fallback for older charms.
1052
if not cmd_exists('function-get'):
1053
cmd = ['action-set']
1055
for k, v in list(values.items()):
1056
cmd.append('{}={}'.format(k, v))
1057
subprocess.check_call(cmd)
778
1060
def action_fail(message):
779
"""Sets the action status to failed and sets the error message.
1062
Sets the action status to failed and sets the error message.
781
The results set by action_set are preserved."""
1064
The results set by action_set are preserved.
782
1066
subprocess.check_call(['action-fail', message])
1069
@deprecate("moved to action_fail()", log=log)
1070
def function_fail(message):
1073
Sets the function status to failed and sets the error message.
1075
The results set by function_set are preserved.
1077
cmd = ['function-fail']
1078
# Fallback for older charms.
1079
if not cmd_exists('function-fail'):
1080
cmd = ['action-fail']
1083
subprocess.check_call(cmd)
785
1086
def action_name():
786
1087
"""Get the name of the currently executing action."""
787
1088
return os.environ.get('JUJU_ACTION_NAME')
1091
def function_name():
1092
"""Get the name of the currently executing function."""
1093
return os.environ.get('JUJU_FUNCTION_NAME') or action_name()
790
1096
def action_uuid():
791
1097
"""Get the UUID of the currently executing action."""
792
1098
return os.environ.get('JUJU_ACTION_UUID')
1102
"""Get the ID of the currently executing function."""
1103
return os.environ.get('JUJU_FUNCTION_ID') or action_uuid()
795
1106
def action_tag():
796
1107
"""Get the tag for the currently executing action."""
797
1108
return os.environ.get('JUJU_ACTION_TAG')
800
def status_set(workload_state, message):
1112
"""Get the tag for the currently executing function."""
1113
return os.environ.get('JUJU_FUNCTION_TAG') or action_tag()
1116
def status_set(workload_state, message, application=False):
801
1117
"""Set the workload state with a message
803
1119
Use status-set to set the workload state with a message which is visible
804
1120
to the user via juju status. If the status-set command is not found then
805
assume this is juju < 1.23 and juju-log the message unstead.
1121
assume this is juju < 1.23 and juju-log the message instead.
807
workload_state -- valid juju workload state.
808
message -- status update message
1123
workload_state -- valid juju workload state. str or WORKLOAD_STATES
1124
message -- status update message
1125
application -- Whether this is an application state set
810
valid_states = ['maintenance', 'blocked', 'waiting', 'active']
811
if workload_state not in valid_states:
813
'{!r} is not a valid workload state'.format(workload_state)
815
cmd = ['status-set', workload_state, message]
1127
bad_state_msg = '{!r} is not a valid workload state'
1129
if isinstance(workload_state, str):
1131
# Convert string to enum.
1132
workload_state = WORKLOAD_STATES[workload_state.upper()]
1134
raise ValueError(bad_state_msg.format(workload_state))
1136
if workload_state not in WORKLOAD_STATES:
1137
raise ValueError(bad_state_msg.format(workload_state))
1139
cmd = ['status-set']
1141
cmd.append('--application')
1142
cmd.extend([workload_state.value, message])
817
1144
ret = subprocess.call(cmd)
1034
1370
:raise: NotImplementedError if run on Juju < 2.0
1036
1372
cmd = ['network-get', '--primary-address', binding]
1037
return subprocess.check_output(cmd).decode('UTF-8').strip()
1374
response = subprocess.check_output(
1376
stderr=subprocess.STDOUT).decode('UTF-8').strip()
1377
except CalledProcessError as e:
1378
if 'no network config found for binding' in e.output.decode('UTF-8'):
1379
raise NoNetworkBinding("No network binding for {}"
1386
def network_get(endpoint, relation_id=None):
1388
Retrieve the network details for a relation endpoint
1390
:param endpoint: string. The name of a relation endpoint
1391
:param relation_id: int. The ID of the relation for the current context.
1392
:return: dict. The loaded YAML output of the network-get query.
1393
:raise: NotImplementedError if request not supported by the Juju version.
1395
if not has_juju_version('2.2'):
1396
raise NotImplementedError(juju_version()) # earlier versions require --primary-address
1397
if relation_id and not has_juju_version('2.3'):
1398
raise NotImplementedError # 2.3 added the -r option
1400
cmd = ['network-get', endpoint, '--format', 'yaml']
1403
cmd.append(relation_id)
1404
response = subprocess.check_output(
1406
stderr=subprocess.STDOUT).decode('UTF-8').strip()
1407
return yaml.safe_load(response)
1410
def add_metric(*args, **kwargs):
1411
"""Add metric values. Values may be expressed with keyword arguments. For
1412
metric names containing dashes, these may be expressed as one or more
1413
'key=value' positional arguments. May only be called from the collect-metrics
1415
_args = ['add-metric']
1417
_kvpairs.extend(args)
1418
_kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1419
_args.extend(sorted(_kvpairs))
1421
subprocess.check_call(_args)
1423
except EnvironmentError as e:
1424
if e.errno != errno.ENOENT:
1426
log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1427
log(log_message, level='INFO')
1431
"""Get the meter status, if running in the meter-status-changed hook."""
1432
return os.environ.get('JUJU_METER_STATUS')
1436
"""Get the meter status information, if running in the meter-status-changed
1438
return os.environ.get('JUJU_METER_INFO')
1441
def iter_units_for_relation_name(relation_name):
1442
"""Iterate through all units in a relation
1444
Generator that iterates through all the units in a relation and yields
1445
a named tuple with rid and unit field names.
1448
data = [(u.rid, u.unit)
1449
for u in iter_units_for_relation_name(relation_name)]
1451
:param relation_name: string relation name
1452
:yield: Named Tuple with rid and unit field names
1454
RelatedUnit = namedtuple('RelatedUnit', 'rid, unit')
1455
for rid in relation_ids(relation_name):
1456
for unit in related_units(rid):
1457
yield RelatedUnit(rid, unit)
1460
def ingress_address(rid=None, unit=None):
1462
Retrieve the ingress-address from a relation when available.
1463
Otherwise, return the private-address.
1465
When used on the consuming side of the relation (unit is a remote
1466
unit), the ingress-address is the IP address that this unit needs
1467
to use to reach the provided service on the remote unit.
1469
When used on the providing side of the relation (unit == local_unit()),
1470
the ingress-address is the IP address that is advertised to remote
1471
units on this relation. Remote units need to use this address to
1472
reach the local provided service on this unit.
1474
Note that charms may document some other method to use in
1475
preference to the ingress_address(), such as an address provided
1476
on a different relation attribute or a service discovery mechanism.
1477
This allows charms to redirect inbound connections to their peers
1478
or different applications such as load balancers.
1481
addresses = [ingress_address(rid=u.rid, unit=u.unit)
1482
for u in iter_units_for_relation_name(relation_name)]
1484
:param rid: string relation id
1485
:param unit: string unit name
1486
:side effect: calls relation_get
1487
:return: string IP address
1489
settings = relation_get(rid=rid, unit=unit)
1490
return (settings.get('ingress-address') or
1491
settings.get('private-address'))
1494
def egress_subnets(rid=None, unit=None):
1496
Retrieve the egress-subnets from a relation.
1498
This function is to be used on the providing side of the
1499
relation, and provides the ranges of addresses that client
1500
connections may come from. The result is uninteresting on
1501
the consuming side of a relation (unit == local_unit()).
1503
Returns a stable list of subnets in CIDR format.
1504
eg. ['192.168.1.0/24', '2001::F00F/128']
1506
If egress-subnets is not available, falls back to using the published
1507
ingress-address, or finally private-address.
1509
:param rid: string relation id
1510
:param unit: string unit name
1511
:side effect: calls relation_get
1512
:return: list of subnets in CIDR format. eg. ['192.168.1.0/24', '2001::F00F/128']
1514
def _to_range(addr):
1515
if re.search(r'^(?:\d{1,3}\.){3}\d{1,3}$', addr) is not None:
1517
elif ':' in addr and '/' not in addr: # IPv6
1521
settings = relation_get(rid=rid, unit=unit)
1522
if 'egress-subnets' in settings:
1523
return [n.strip() for n in settings['egress-subnets'].split(',') if n.strip()]
1524
if 'ingress-address' in settings:
1525
return [_to_range(settings['ingress-address'])]
1526
if 'private-address' in settings:
1527
return [_to_range(settings['private-address'])]
1528
return [] # Should never happen
1531
def unit_doomed(unit=None):
1532
"""Determines if the unit is being removed from the model
1534
Requires Juju 2.4.1.
1536
:param unit: string unit name, defaults to local_unit
1537
:side effect: calls goal_state
1538
:side effect: calls local_unit
1539
:side effect: calls has_juju_version
1540
:return: True if the unit is being removed, already gone, or never existed
1542
if not has_juju_version("2.4.1"):
1543
# We cannot risk blindly returning False for 'we don't know',
1544
# because that could cause data loss; if call sites don't
1545
# need an accurate answer, they likely don't need this helper
1547
# goal-state existed in 2.4.0, but did not handle removals
1548
# correctly until 2.4.1.
1549
raise NotImplementedError("is_doomed")
1553
units = gs.get('units', {})
1554
if unit not in units:
1556
# I don't think 'dead' units ever show up in the goal-state, but
1557
# check anyway in addition to 'dying'.
1558
return units[unit]['status'] in ('dying', 'dead')
1561
def env_proxy_settings(selected_settings=None):
1562
"""Get proxy settings from process environment variables.
1564
Get charm proxy settings from environment variables that correspond to
1565
juju-http-proxy, juju-https-proxy juju-no-proxy (available as of 2.4.2, see
1566
lp:1782236) and juju-ftp-proxy in a format suitable for passing to an
1567
application that reacts to proxy settings passed as environment variables.
1568
Some applications support lowercase or uppercase notation (e.g. curl), some
1569
support only lowercase (e.g. wget), there are also subjectively rare cases
1570
of only uppercase notation support. no_proxy CIDR and wildcard support also
1571
varies between runtimes and applications as there is no enforced standard.
1573
Some applications may connect to multiple destinations and expose config
1574
options that would affect only proxy settings for a specific destination
1575
these should be handled in charms in an application-specific manner.
1577
:param selected_settings: format only a subset of possible settings
1578
:type selected_settings: list
1579
:rtype: Option(None, dict[str, str])
1581
SUPPORTED_SETTINGS = {
1582
'http': 'HTTP_PROXY',
1583
'https': 'HTTPS_PROXY',
1584
'no_proxy': 'NO_PROXY',
1587
if selected_settings is None:
1588
selected_settings = SUPPORTED_SETTINGS
1590
selected_vars = [v for k, v in SUPPORTED_SETTINGS.items()
1591
if k in selected_settings]
1593
for var in selected_vars:
1594
var_val = os.getenv(var)
1596
proxy_settings[var] = var_val
1597
proxy_settings[var.lower()] = var_val
1598
# Now handle juju-prefixed environment variables. The legacy vs new
1599
# environment variable usage is mutually exclusive
1600
charm_var_val = os.getenv('JUJU_CHARM_{}'.format(var))
1602
proxy_settings[var] = charm_var_val
1603
proxy_settings[var.lower()] = charm_var_val
1604
if 'no_proxy' in proxy_settings:
1605
if _contains_range(proxy_settings['no_proxy']):
1606
log(RANGE_WARNING, level=WARNING)
1607
return proxy_settings if proxy_settings else None
1610
def _contains_range(addresses):
1611
"""Check for cidr or wildcard domain in a string.
1613
Given a string comprising a comma separated list of ip addresses
1614
and domain names, determine whether the string contains IP ranges
1615
or wildcard domains.
1617
:param addresses: comma separated list of domains and ip addresses.
1618
:type addresses: str
1621
# Test for cidr (e.g. 10.20.20.0/24)
1623
# Test for wildcard domains (*.foo.com or .foo.com)
1625
addresses.startswith(".") or
1626
",." in addresses or
1630
def is_subordinate():
1631
"""Check whether charm is subordinate in unit metadata.
1633
:returns: True if unit is subordniate, False otherwise.
1636
return metadata().get('subordinate') is True