Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
vnf-onboarding
OSM Packages
Commits
17583c8b
Commit
17583c8b
authored
Nov 30, 2020
by
Mark Beierl
Browse files
Merge branch 'master' into 'master'
Update squid charm See merge request
!105
parents
e2197769
9c012576
Pipeline
#143
passed with stage
in 1 minute and 34 seconds
Changes
160
Pipelines
2
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
0 additions
and
4427 deletions
+0
-4427
magma/squid_cnf/charms/squid/mod/operator/docs/index.rst
magma/squid_cnf/charms/squid/mod/operator/docs/index.rst
+0
-58
magma/squid_cnf/charms/squid/mod/operator/docs/requirements.txt
...squid_cnf/charms/squid/mod/operator/docs/requirements.txt
+0
-1
magma/squid_cnf/charms/squid/mod/operator/ops/framework.py
magma/squid_cnf/charms/squid/mod/operator/ops/framework.py
+0
-1134
magma/squid_cnf/charms/squid/mod/operator/ops/jujuversion.py
magma/squid_cnf/charms/squid/mod/operator/ops/jujuversion.py
+0
-85
magma/squid_cnf/charms/squid/mod/operator/ops/main.py
magma/squid_cnf/charms/squid/mod/operator/ops/main.py
+0
-235
magma/squid_cnf/charms/squid/mod/operator/ops/testing.py
magma/squid_cnf/charms/squid/mod/operator/ops/testing.py
+0
-477
magma/squid_cnf/charms/squid/mod/operator/requirements.txt
magma/squid_cnf/charms/squid/mod/operator/requirements.txt
+0
-1
magma/squid_cnf/charms/squid/mod/operator/run_tests
magma/squid_cnf/charms/squid/mod/operator/run_tests
+0
-3
magma/squid_cnf/charms/squid/mod/operator/setup.py
magma/squid_cnf/charms/squid/mod/operator/setup.py
+0
-39
magma/squid_cnf/charms/squid/mod/operator/test/__init__.py
magma/squid_cnf/charms/squid/mod/operator/test/__init__.py
+0
-0
magma/squid_cnf/charms/squid/mod/operator/test/bin/relation-ids
...squid_cnf/charms/squid/mod/operator/test/bin/relation-ids
+0
-11
magma/squid_cnf/charms/squid/mod/operator/test/bin/relation-list
...quid_cnf/charms/squid/mod/operator/test/bin/relation-list
+0
-16
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/config.yaml
...arms/squid/mod/operator/test/charms/test_main/config.yaml
+0
-1
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/lib/__init__.py
.../squid/mod/operator/test/charms/test_main/lib/__init__.py
+0
-0
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/lib/ops
...f/charms/squid/mod/operator/test/charms/test_main/lib/ops
+0
-1
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/metadata.yaml
...ms/squid/mod/operator/test/charms/test_main/metadata.yaml
+0
-26
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/src/charm.py
...rms/squid/mod/operator/test/charms/test_main/src/charm.py
+0
-208
magma/squid_cnf/charms/squid/mod/operator/test/test_charm.py
magma/squid_cnf/charms/squid/mod/operator/test/test_charm.py
+0
-323
magma/squid_cnf/charms/squid/mod/operator/test/test_framework.py
...quid_cnf/charms/squid/mod/operator/test/test_framework.py
+0
-1727
magma/squid_cnf/charms/squid/mod/operator/test/test_helpers.py
.../squid_cnf/charms/squid/mod/operator/test/test_helpers.py
+0
-81
No files found.
magma/squid_cnf/charms/squid/mod/operator/docs/index.rst
deleted
100644 → 0
View file @
e2197769
Welcome to The Operator Framework's documentation!
==================================================
.. toctree::
:maxdepth: 2
:caption: Contents:
ops package
===========
.. automodule:: ops
Submodules
----------
ops.charm module
----------------
.. automodule:: ops.charm
ops.framework module
--------------------
.. automodule:: ops.framework
ops.jujuversion module
----------------------
.. automodule:: ops.jujuversion
ops.log module
--------------
.. automodule:: ops.log
ops.main module
---------------
.. automodule:: ops.main
ops.model module
----------------
.. automodule:: ops.model
ops.testing module
------------------
.. automodule:: ops.testing
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
magma/squid_cnf/charms/squid/mod/operator/docs/requirements.txt
deleted
100644 → 0
View file @
e2197769
sphinx<2
magma/squid_cnf/charms/squid/mod/operator/ops/framework.py
deleted
100755 → 0
View file @
e2197769
This diff is collapsed.
Click to expand it.
magma/squid_cnf/charms/squid/mod/operator/ops/jujuversion.py
deleted
100755 → 0
View file @
e2197769
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import
re
from
functools
import
total_ordering
@
total_ordering
class
JujuVersion
:
PATTERN
=
r
'''^
(?P<major>\d{1,9})\.(?P<minor>\d{1,9}) # <major> and <minor> numbers are always there
((?:\.|-(?P<tag>[a-z]+))(?P<patch>\d{1,9}))? # sometimes with .<patch> or -<tag><patch>
(\.(?P<build>\d{1,9}))?$ # and sometimes with a <build> number.
'''
def
__init__
(
self
,
version
):
m
=
re
.
match
(
self
.
PATTERN
,
version
,
re
.
VERBOSE
)
if
not
m
:
raise
RuntimeError
(
'"{}" is not a valid Juju version string'
.
format
(
version
))
d
=
m
.
groupdict
()
self
.
major
=
int
(
m
.
group
(
'major'
))
self
.
minor
=
int
(
m
.
group
(
'minor'
))
self
.
tag
=
d
[
'tag'
]
or
''
self
.
patch
=
int
(
d
[
'patch'
]
or
0
)
self
.
build
=
int
(
d
[
'build'
]
or
0
)
def
__repr__
(
self
):
if
self
.
tag
:
s
=
'{}.{}-{}{}'
.
format
(
self
.
major
,
self
.
minor
,
self
.
tag
,
self
.
patch
)
else
:
s
=
'{}.{}.{}'
.
format
(
self
.
major
,
self
.
minor
,
self
.
patch
)
if
self
.
build
>
0
:
s
+=
'.{}'
.
format
(
self
.
build
)
return
s
def
__eq__
(
self
,
other
):
if
self
is
other
:
return
True
if
isinstance
(
other
,
str
):
other
=
type
(
self
)(
other
)
elif
not
isinstance
(
other
,
JujuVersion
):
raise
RuntimeError
(
'cannot compare Juju version "{}" with "{}"'
.
format
(
self
,
other
))
return
(
self
.
major
==
other
.
major
and
self
.
minor
==
other
.
minor
and
self
.
tag
==
other
.
tag
and
self
.
build
==
other
.
build
and
self
.
patch
==
other
.
patch
)
def
__lt__
(
self
,
other
):
if
self
is
other
:
return
False
if
isinstance
(
other
,
str
):
other
=
type
(
self
)(
other
)
elif
not
isinstance
(
other
,
JujuVersion
):
raise
RuntimeError
(
'cannot compare Juju version "{}" with "{}"'
.
format
(
self
,
other
))
if
self
.
major
!=
other
.
major
:
return
self
.
major
<
other
.
major
elif
self
.
minor
!=
other
.
minor
:
return
self
.
minor
<
other
.
minor
elif
self
.
tag
!=
other
.
tag
:
if
not
self
.
tag
:
return
False
elif
not
other
.
tag
:
return
True
return
self
.
tag
<
other
.
tag
elif
self
.
patch
!=
other
.
patch
:
return
self
.
patch
<
other
.
patch
elif
self
.
build
!=
other
.
build
:
return
self
.
build
<
other
.
build
return
False
magma/squid_cnf/charms/squid/mod/operator/ops/main.py
deleted
100755 → 0
View file @
e2197769
#!/usr/bin/env python3
# Copyright 2019 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import
os
import
subprocess
import
sys
from
pathlib
import
Path
import
yaml
import
ops.charm
import
ops.framework
import
ops.model
import
logging
from
ops.log
import
setup_root_logging
CHARM_STATE_FILE
=
'.unit-state.db'
logger
=
logging
.
getLogger
()
def
_get_charm_dir
():
charm_dir
=
os
.
environ
.
get
(
"JUJU_CHARM_DIR"
)
if
charm_dir
is
None
:
# Assume $JUJU_CHARM_DIR/lib/op/main.py structure.
charm_dir
=
Path
(
'{}/../../..'
.
format
(
__file__
)).
resolve
()
else
:
charm_dir
=
Path
(
charm_dir
).
resolve
()
return
charm_dir
def
_load_metadata
(
charm_dir
):
metadata
=
yaml
.
safe_load
((
charm_dir
/
'metadata.yaml'
).
read_text
())
actions_meta
=
charm_dir
/
'actions.yaml'
if
actions_meta
.
exists
():
actions_metadata
=
yaml
.
safe_load
(
actions_meta
.
read_text
())
else
:
actions_metadata
=
{}
return
metadata
,
actions_metadata
def
_create_event_link
(
charm
,
bound_event
):
"""Create a symlink for a particular event.
charm -- A charm object.
bound_event -- An event for which to create a symlink.
"""
if
issubclass
(
bound_event
.
event_type
,
ops
.
charm
.
HookEvent
):
event_dir
=
charm
.
framework
.
charm_dir
/
'hooks'
event_path
=
event_dir
/
bound_event
.
event_kind
.
replace
(
'_'
,
'-'
)
elif
issubclass
(
bound_event
.
event_type
,
ops
.
charm
.
ActionEvent
):
if
not
bound_event
.
event_kind
.
endswith
(
"_action"
):
raise
RuntimeError
(
'action event name {} needs _action suffix'
.
format
(
bound_event
.
event_kind
))
event_dir
=
charm
.
framework
.
charm_dir
/
'actions'
# The event_kind is suffixed with "_action" while the executable is not.
event_path
=
event_dir
/
bound_event
.
event_kind
[:
-
len
(
'_action'
)].
replace
(
'_'
,
'-'
)
else
:
raise
RuntimeError
(
'cannot create a symlink: unsupported event type {}'
.
format
(
bound_event
.
event_type
))
event_dir
.
mkdir
(
exist_ok
=
True
)
if
not
event_path
.
exists
():
# CPython has different implementations for populating sys.argv[0] for Linux and Windows.
# For Windows it is always an absolute path (any symlinks are resolved)
# while for Linux it can be a relative path.
target_path
=
os
.
path
.
relpath
(
os
.
path
.
realpath
(
sys
.
argv
[
0
]),
str
(
event_dir
))
# Ignore the non-symlink files or directories
# assuming the charm author knows what they are doing.
logger
.
debug
(
'Creating a new relative symlink at %s pointing to %s'
,
event_path
,
target_path
)
event_path
.
symlink_to
(
target_path
)
def
_setup_event_links
(
charm_dir
,
charm
):
"""Set up links for supported events that originate from Juju.
Whether a charm can handle an event or not can be determined by
introspecting which events are defined on it.
Hooks or actions are created as symlinks to the charm code file
which is determined by inspecting symlinks provided by the charm
author at hooks/install or hooks/start.
charm_dir -- A root directory of the charm.
charm -- An instance of the Charm class.
"""
for
bound_event
in
charm
.
on
.
events
().
values
():
# Only events that originate from Juju need symlinks.
if
issubclass
(
bound_event
.
event_type
,
(
ops
.
charm
.
HookEvent
,
ops
.
charm
.
ActionEvent
)):
_create_event_link
(
charm
,
bound_event
)
def
_emit_charm_event
(
charm
,
event_name
):
"""Emits a charm event based on a Juju event name.
charm -- A charm instance to emit an event from.
event_name -- A Juju event name to emit on a charm.
"""
event_to_emit
=
None
try
:
event_to_emit
=
getattr
(
charm
.
on
,
event_name
)
except
AttributeError
:
logger
.
debug
(
"event %s not defined for %s"
,
event_name
,
charm
)
# If the event is not supported by the charm implementation, do
# not error out or try to emit it. This is to support rollbacks.
if
event_to_emit
is
not
None
:
args
,
kwargs
=
_get_event_args
(
charm
,
event_to_emit
)
logger
.
debug
(
'Emitting Juju event %s'
,
event_name
)
event_to_emit
.
emit
(
*
args
,
**
kwargs
)
def
_get_event_args
(
charm
,
bound_event
):
event_type
=
bound_event
.
event_type
model
=
charm
.
framework
.
model
if
issubclass
(
event_type
,
ops
.
charm
.
RelationEvent
):
relation_name
=
os
.
environ
[
'JUJU_RELATION'
]
relation_id
=
int
(
os
.
environ
[
'JUJU_RELATION_ID'
].
split
(
':'
)[
-
1
])
relation
=
model
.
get_relation
(
relation_name
,
relation_id
)
else
:
relation
=
None
remote_app_name
=
os
.
environ
.
get
(
'JUJU_REMOTE_APP'
,
''
)
remote_unit_name
=
os
.
environ
.
get
(
'JUJU_REMOTE_UNIT'
,
''
)
if
remote_app_name
or
remote_unit_name
:
if
not
remote_app_name
:
if
'/'
not
in
remote_unit_name
:
raise
RuntimeError
(
'invalid remote unit name: {}'
.
format
(
remote_unit_name
))
remote_app_name
=
remote_unit_name
.
split
(
'/'
)[
0
]
args
=
[
relation
,
model
.
get_app
(
remote_app_name
)]
if
remote_unit_name
:
args
.
append
(
model
.
get_unit
(
remote_unit_name
))
return
args
,
{}
elif
relation
:
return
[
relation
],
{}
return
[],
{}
def
main
(
charm_class
):
"""Setup the charm and dispatch the observed event.
The event name is based on the way this executable was called (argv[0]).
"""
charm_dir
=
_get_charm_dir
()
model_backend
=
ops
.
model
.
ModelBackend
()
debug
=
(
'JUJU_DEBUG'
in
os
.
environ
)
setup_root_logging
(
model_backend
,
debug
=
debug
)
# Process the Juju event relevant to the current hook execution
# JUJU_HOOK_NAME, JUJU_FUNCTION_NAME, and JUJU_ACTION_NAME are not used
# in order to support simulation of events from debugging sessions.
#
# TODO: For Windows, when symlinks are used, this is not a valid
# method of getting an event name (see LP: #1854505).
juju_exec_path
=
Path
(
sys
.
argv
[
0
])
has_dispatch
=
juju_exec_path
.
name
==
'dispatch'
if
has_dispatch
:
# The executable was 'dispatch', which means the actual hook we want to
# run needs to be looked up in the JUJU_DISPATCH_PATH env var, where it
# should be a path relative to the charm directory (the directory that
# holds `dispatch`). If that path actually exists, we want to run that
# before continuing.
dispatch_path
=
juju_exec_path
.
parent
/
Path
(
os
.
environ
[
'JUJU_DISPATCH_PATH'
])
if
dispatch_path
.
exists
()
and
dispatch_path
.
resolve
()
!=
juju_exec_path
.
resolve
():
argv
=
sys
.
argv
.
copy
()
argv
[
0
]
=
str
(
dispatch_path
)
try
:
subprocess
.
run
(
argv
,
check
=
True
)
except
subprocess
.
CalledProcessError
as
e
:
logger
.
warning
(
"hook %s exited with status %d"
,
dispatch_path
,
e
.
returncode
)
sys
.
exit
(
e
.
returncode
)
juju_exec_path
=
dispatch_path
juju_event_name
=
juju_exec_path
.
name
.
replace
(
'-'
,
'_'
)
if
juju_exec_path
.
parent
.
name
==
'actions'
:
juju_event_name
=
'{}_action'
.
format
(
juju_event_name
)
metadata
,
actions_metadata
=
_load_metadata
(
charm_dir
)
meta
=
ops
.
charm
.
CharmMeta
(
metadata
,
actions_metadata
)
unit_name
=
os
.
environ
[
'JUJU_UNIT_NAME'
]
model
=
ops
.
model
.
Model
(
unit_name
,
meta
,
model_backend
)
# TODO: If Juju unit agent crashes after exit(0) from the charm code
# the framework will commit the snapshot but Juju will not commit its
# operation.
charm_state_path
=
charm_dir
/
CHARM_STATE_FILE
framework
=
ops
.
framework
.
Framework
(
charm_state_path
,
charm_dir
,
meta
,
model
)
try
:
charm
=
charm_class
(
framework
,
None
)
if
not
has_dispatch
:
# When a charm is force-upgraded and a unit is in an error state Juju
# does not run upgrade-charm and instead runs the failed hook followed
# by config-changed. Given the nature of force-upgrading the hook setup
# code is not triggered on config-changed.
#
# 'start' event is included as Juju does not fire the install event for
# K8s charms (see LP: #1854635).
if
(
juju_event_name
in
(
'install'
,
'start'
,
'upgrade_charm'
)
or
juju_event_name
.
endswith
(
'_storage_attached'
)):
_setup_event_links
(
charm_dir
,
charm
)
# TODO: Remove the collect_metrics check below as soon as the relevant
# Juju changes are made.
#
# Skip reemission of deferred events for collect-metrics events because
# they do not have the full access to all hook tools.
if
juju_event_name
!=
'collect_metrics'
:
framework
.
reemit
()
_emit_charm_event
(
charm
,
juju_event_name
)
framework
.
commit
()
finally
:
framework
.
close
()
magma/squid_cnf/charms/squid/mod/operator/ops/testing.py
deleted
100644 → 0
View file @
e2197769
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import
inspect
import
pathlib
from
textwrap
import
dedent
import
typing
from
ops
import
charm
,
framework
,
model
# OptionalYAML is something like metadata.yaml or actions.yaml. You can
# pass in a file-like object or the string directly.
OptionalYAML
=
typing
.
Optional
[
typing
.
Union
[
str
,
typing
.
TextIO
]]
# noinspection PyProtectedMember
class
Harness
:
"""This class represents a way to build up the model that will drive a test suite.
The model that is created is from the viewpoint of the charm that you are testing.
Example::
harness = Harness(MyCharm)
# Do initial setup here
relation_id = harness.add_relation('db', 'postgresql')
# Now instantiate the charm to see events as the model changes
harness.begin()
harness.add_relation_unit(relation_id, 'postgresql/0')
harness.update_relation_data(relation_id, 'postgresql/0', {'key': 'val'})
# Check that charm has properly handled the relation_joined event for postgresql/0
self.assertEqual(harness.charm. ...)
Args:
charm_cls: The Charm class that you'll be testing.
meta: charm.CharmBase is a A string or file-like object containing the contents of
metadata.yaml. If not supplied, we will look for a 'metadata.yaml' file in the
parent directory of the Charm, and if not found fall back to a trivial
'name: test-charm' metadata.
actions: A string or file-like object containing the contents of
actions.yaml. If not supplied, we will look for a 'actions.yaml' file in the
parent directory of the Charm.
"""
def
__init__
(
self
,
charm_cls
:
typing
.
Type
[
charm
.
CharmBase
],
*
,
meta
:
OptionalYAML
=
None
,
actions
:
OptionalYAML
=
None
):
# TODO: jam 2020-03-05 We probably want to take config as a parameter as well, since
# it would define the default values of config that the charm would see.
self
.
_charm_cls
=
charm_cls
self
.
_charm
=
None
self
.
_charm_dir
=
'no-disk-path'
# this may be updated by _create_meta
self
.
_meta
=
self
.
_create_meta
(
meta
,
actions
)
self
.
_unit_name
=
self
.
_meta
.
name
+
'/0'
self
.
_framework
=
None
self
.
_hooks_enabled
=
True
self
.
_relation_id_counter
=
0
self
.
_backend
=
_TestingModelBackend
(
self
.
_unit_name
,
self
.
_meta
)
self
.
_model
=
model
.
Model
(
self
.
_unit_name
,
self
.
_meta
,
self
.
_backend
)
self
.
_framework
=
framework
.
Framework
(
":memory:"
,
self
.
_charm_dir
,
self
.
_meta
,
self
.
_model
)
@
property
def
charm
(
self
)
->
charm
.
CharmBase
:
"""Return the instance of the charm class that was passed to __init__.
Note that the Charm is not instantiated until you have called
:meth:`.begin()`.
"""
return
self
.
_charm
@
property
def
model
(
self
)
->
model
.
Model
:
"""Return the :class:`~ops.model.Model` that is being driven by this Harness."""
return
self
.
_model
@
property
def
framework
(
self
)
->
framework
.
Framework
:
"""Return the Framework that is being driven by this Harness."""
return
self
.
_framework
def
begin
(
self
)
->
None
:
"""Instantiate the Charm and start handling events.
Before calling begin(), there is no Charm instance, so changes to the Model won't emit
events. You must call begin before :attr:`.charm` is valid.
"""
if
self
.
_charm
is
not
None
:
raise
RuntimeError
(
'cannot call the begin method on the harness more than once'
)
# The Framework adds attributes to class objects for events, etc. As such, we can't re-use
# the original class against multiple Frameworks. So create a locally defined class
# and register it.
# TODO: jam 2020-03-16 We are looking to changes this to Instance attributes instead of
# Class attributes which should clean up this ugliness. The API can stay the same
class
TestEvents
(
self
.
_charm_cls
.
on
.
__class__
):
pass
TestEvents
.
__name__
=
self
.
_charm_cls
.
on
.
__class__
.
__name__
class
TestCharm
(
self
.
_charm_cls
):
on
=
TestEvents
()
# Note: jam 2020-03-01 This is so that errors in testing say MyCharm has no attribute foo,
# rather than TestCharm has no attribute foo.
TestCharm
.
__name__
=
self
.
_charm_cls
.
__name__
self
.
_charm
=
TestCharm
(
self
.
_framework
,
self
.
_framework
.
meta
.
name
)
def
_create_meta
(
self
,
charm_metadata
,
action_metadata
):
"""Create a CharmMeta object.
Handle the cases where a user doesn't supply explicit metadata snippets.
"""
filename
=
inspect
.
getfile
(
self
.
_charm_cls
)
charm_dir
=
pathlib
.
Path
(
filename
).
parents
[
1
]
if
charm_metadata
is
None
:
metadata_path
=
charm_dir
/
'metadata.yaml'
if
metadata_path
.
is_file
():
charm_metadata
=
metadata_path
.
read_text
()
self
.
_charm_dir
=
charm_dir
else
:
# The simplest of metadata that the framework can support
charm_metadata
=
'name: test-charm'
elif
isinstance
(
charm_metadata
,
str
):
charm_metadata
=
dedent
(
charm_metadata
)
if
action_metadata
is
None
:
actions_path
=
charm_dir
/
'actions.yaml'
if
actions_path
.
is_file
():
action_metadata
=
actions_path
.
read_text
()
self
.
_charm_dir
=
charm_dir
elif
isinstance
(
action_metadata
,
str
):
action_metadata
=
dedent
(
action_metadata
)
return
charm
.
CharmMeta
.
from_yaml
(
charm_metadata
,
action_metadata
)
def
disable_hooks
(
self
)
->
None
:
"""Stop emitting hook events when the model changes.
This can be used by developers to stop changes to the model from emitting events that
the charm will react to. Call :meth:`.enable_hooks`
to re-enable them.
"""
self
.
_hooks_enabled
=
False
def
enable_hooks
(
self
)
->
None
:
"""Re-enable hook events from charm.on when the model is changed.
By default hook events are enabled once you call :meth:`.begin`,
but if you have used :meth:`.disable_hooks`, this can be used to
enable them again.
"""
self
.
_hooks_enabled
=
True
def
_next_relation_id
(
self
):
rel_id
=
self
.
_relation_id_counter
self
.
_relation_id_counter
+=
1
return
rel_id
def
add_relation
(
self
,
relation_name
:
str
,
remote_app
:
str
)
->
int
:
"""Declare that there is a new relation between this app and `remote_app`.
Args:
relation_name: The relation on Charm that is being related to
remote_app: The name of the application that is being related to
Return:
The relation_id created by this add_relation.
"""
rel_id
=
self
.
_next_relation_id
()
self
.
_backend
.
_relation_ids_map
.
setdefault
(
relation_name
,
[]).
append
(
rel_id
)
self
.
_backend
.
_relation_names
[
rel_id
]
=
relation_name
self
.
_backend
.
_relation_list_map
[
rel_id
]
=
[]
self
.
_backend
.
_relation_data
[
rel_id
]
=
{
remote_app
:
{},
self
.
_backend
.
unit_name
:
{},
self
.
_backend
.
app_name
:
{},
}
# Reload the relation_ids list
if
self
.
_model
is
not
None
:
self
.
_model
.
relations
.
_invalidate
(
relation_name
)
if
self
.
_charm
is
None
or
not
self
.
_hooks_enabled
:
return
rel_id
relation
=
self
.
_model
.
get_relation
(
relation_name
,
rel_id
)
app
=
self
.
_model
.
get_app
(
remote_app
)
self
.
_charm
.
on
[
relation_name
].
relation_created
.
emit
(
relation
,
app
)
return
rel_id
def
add_relation_unit
(
self
,
relation_id
:
int
,
remote_unit_name
:
str
)
->
None
:
"""Add a new unit to a relation.
Example::
rel_id = harness.add_relation('db', 'postgresql')
harness.add_relation_unit(rel_id, 'postgresql/0')
This will trigger a `relation_joined` event and a `relation_changed` event.
Args:
relation_id: The integer relation identifier (as returned by add_relation).
remote_unit_name: A string representing the remote unit that is being added.
Return:
None
"""
self
.
_backend
.
_relation_list_map
[
relation_id
].
append
(
remote_unit_name
)
self
.
_backend
.
_relation_data
[
relation_id
][
remote_unit_name
]
=
{}
relation_name
=
self
.
_backend
.
_relation_names
[
relation_id
]
# Make sure that the Model reloads the relation_list for this relation_id, as well as
# reloading the relation data for this unit.
if
self
.
_model
is
not
None
:
self
.
_model
.
relations
.
_invalidate
(
relation_name
)
remote_unit
=
self
.
_model
.
get_unit
(
remote_unit_name
)
relation
=
self
.
_model
.
get_relation
(
relation_name
,
relation_id
)
relation
.
data
[
remote_unit
].
_invalidate
()
if
self
.
_charm
is
None
or
not
self
.
_hooks_enabled
:
return
self
.
_charm
.
on
[
relation_name
].
relation_joined
.
emit
(
relation
,
remote_unit
.
app
,
remote_unit
)
def
get_relation_data
(
self
,
relation_id
:
int
,
app_or_unit
:
str
)
->
typing
.
Mapping
:
"""Get the relation data bucket for a single app or unit in a given relation.
This ignores all of the safety checks of who can and can't see data in relations (eg,
non-leaders can't read their own application's relation data because there are no events
that keep that data up-to-date for the unit).
Args:
relation_id: The relation whose content we want to look at.
app_or_unit: The name of the application or unit whose data we want to read
Return:
a dict containing the relation data for `app_or_unit` or None.
Raises:
KeyError: if relation_id doesn't exist
"""
return
self
.
_backend
.
_relation_data
[
relation_id
].
get
(
app_or_unit
,
None
)
def
get_workload_version
(
self
)
->
str
:
"""Read the workload version that was set by the unit."""
return
self
.
_backend
.
_workload_version
def
update_relation_data
(
self
,
relation_id
:
int
,
app_or_unit
:
str
,
key_values
:
typing
.
Mapping
,
)
->
None
:
"""Update the relation data for a given unit or application in a given relation.
This also triggers the `relation_changed` event for this relation_id.
Args:
relation_id: The integer relation_id representing this relation.
app_or_unit: The unit or application name that is being updated.
This can be the local or remote application.
key_values: Each key/value will be updated in the relation data.
"""
relation_name
=
self
.
_backend
.
_relation_names
[
relation_id
]
relation
=
self
.
_model
.
get_relation
(
relation_name
,
relation_id
)
if
'/'
in
app_or_unit
:
entity
=
self
.
_model
.
get_unit
(
app_or_unit
)
else
:
entity
=
self
.
_model
.
get_app
(
app_or_unit
)
rel_data
=
relation
.
data
.
get
(
entity
,
None
)
if
rel_data
is
not
None
:
# rel_data may have cached now-stale data, so _invalidate() it.
# Note, this won't cause the data to be loaded if it wasn't already.
rel_data
.
_invalidate
()
new_values
=
self
.
_backend
.
_relation_data
[
relation_id
][
app_or_unit
].
copy
()
for
k
,
v
in
key_values
.
items
():
if
v
==
''
:
new_values
.
pop
(
k
,
None
)
else
:
new_values
[
k
]
=
v
self
.
_backend
.
_relation_data
[
relation_id
][
app_or_unit
]
=
new_values
if
app_or_unit
==
self
.
_model
.
unit
.
name
:
# No events for our own unit
return
if
app_or_unit
==
self
.
_model
.
app
.
name
:
# updating our own app only generates an event if it is a peer relation and we
# aren't the leader
is_peer
=
self
.
_meta
.
relations
[
relation_name
].
role
==
'peers'
if
not
is_peer
:
return
if
self
.
_model
.
unit
.
is_leader
():
return
self
.
_emit_relation_changed
(
relation_id
,
app_or_unit
)
def
_emit_relation_changed
(
self
,
relation_id
,
app_or_unit
):
if
self
.
_charm
is
None
or
not
self
.
_hooks_enabled
:
return
rel_name
=
self
.
_backend
.
_relation_names
[
relation_id
]
relation
=
self
.
model
.
get_relation
(
rel_name
,
relation_id
)
if
'/'
in
app_or_unit
:
app_name
=
app_or_unit
.
split
(
'/'
)[
0
]
unit_name
=
app_or_unit
app
=
self
.
model
.
get_app
(
app_name
)
unit
=
self
.
model
.
get_unit
(
unit_name
)
args
=
(
relation
,
app
,
unit
)
else
:
app_name
=
app_or_unit
app
=
self
.
model
.
get_app
(
app_name
)
args
=
(
relation
,
app
)
self
.
_charm
.
on
[
rel_name
].
relation_changed
.
emit
(
*
args
)
def
update_config
(
self
,
key_values
:
typing
.
Mapping
[
str
,
str
]
=
None
,
unset
:
typing
.
Iterable
[
str
]
=
(),
)
->
None
:
"""Update the config as seen by the charm.
This will trigger a `config_changed` event.
Args:
key_values: A Mapping of key:value pairs to update in config.
unset: An iterable of keys to remove from Config. (Note that this does
not currently reset the config values to the default defined in config.yaml.)
"""
config
=
self
.
_backend
.
_config
if
key_values
is
not
None
:
for
key
,
value
in
key_values
.
items
():
config
[
key
]
=
value
for
key
in
unset
:
config
.
pop
(
key
,
None
)
# NOTE: jam 2020-03-01 Note that this sort of works "by accident". Config
# is a LazyMapping, but its _load returns a dict and this method mutates
# the dict that Config is caching. Arguably we should be doing some sort
# of charm.framework.model.config._invalidate()
if
self
.
_charm
is
None
or
not
self
.
_hooks_enabled
:
return
self
.
_charm
.
on
.
config_changed
.
emit
()
def
set_leader
(
self
,
is_leader
:
bool
=
True
)
->
None
:
"""Set whether this unit is the leader or not.
If this charm becomes a leader then `leader_elected` will be triggered.
Args:
is_leader: True/False as to whether this unit is the leader.
"""
was_leader
=
self
.
_backend
.
_is_leader
self
.
_backend
.
_is_leader
=
is_leader
# Note: jam 2020-03-01 currently is_leader is cached at the ModelBackend level, not in
# the Model objects, so this automatically gets noticed.
if
is_leader
and
not
was_leader
and
self
.
_charm
is
not
None
and
self
.
_hooks_enabled
:
self
.
_charm
.
on
.
leader_elected
.
emit
()
class
_TestingModelBackend
:
"""This conforms to the interface for ModelBackend but provides canned data.
DO NOT use this class directly, it is used by `Harness`_ to drive the model.
`Harness`_ is responsible for maintaining the internal consistency of the values here,
as the only public methods of this type are for implementing ModelBackend.
"""
def
__init__
(
self
,
unit_name
,
meta
):
self
.
unit_name
=
unit_name
self
.
app_name
=
self
.
unit_name
.
split
(
'/'
)[
0
]
self
.
_calls
=
[]
self
.
_meta
=
meta
self
.
_is_leader
=
None
self
.
_relation_ids_map
=
{}
# relation name to [relation_ids,...]
self
.
_relation_names
=
{}
# reverse map from relation_id to relation_name
self
.
_relation_list_map
=
{}
# relation_id: [unit_name,...]
self
.
_relation_data
=
{}
# {relation_id: {name: data}}
self
.
_config
=
{}
self
.
_is_leader
=
False
self
.
_resources_map
=
{}
self
.
_pod_spec
=
None
self
.
_app_status
=
None
self
.
_unit_status
=
None
self
.
_workload_version
=
None
def
relation_ids
(
self
,
relation_name
):
try
:
return
self
.
_relation_ids_map
[
relation_name
]
except
KeyError
as
e
:
if
relation_name
not
in
self
.
_meta
.
relations
:
raise
model
.
ModelError
(
'{} is not a known relation'
.
format
(
relation_name
))
from
e
return
[]
def
relation_list
(
self
,
relation_id
):
try
:
return
self
.
_relation_list_map
[
relation_id
]
except
KeyError
as
e
:
raise
model
.
RelationNotFoundError
from
e
def
relation_get
(
self
,
relation_id
,
member_name
,
is_app
):
if
is_app
and
'/'
in
member_name
:
member_name
=
member_name
.
split
(
'/'
)[
0
]
if
relation_id
not
in
self
.
_relation_data
:
raise
model
.
RelationNotFoundError
()
return
self
.
_relation_data
[
relation_id
][
member_name
].
copy
()
def
relation_set
(
self
,
relation_id
,
key
,
value
,
is_app
):
relation
=
self
.
_relation_data
[
relation_id
]
if
is_app
:
bucket_key
=
self
.
app_name
else
:
bucket_key
=
self
.
unit_name
if
bucket_key
not
in
relation
:
relation
[
bucket_key
]
=
{}
bucket
=
relation
[
bucket_key
]
if
value
==
''
:
bucket
.
pop
(
key
,
None
)
else
:
bucket
[
key
]
=
value
def
config_get
(
self
):
return
self
.
_config
def
is_leader
(
self
):
return
self
.
_is_leader
def
application_version_set
(
self
,
version
):
self
.
_workload_version
=
version
def
resource_get
(
self
,
resource_name
):
return
self
.
_resources_map
[
resource_name
]
def
pod_spec_set
(
self
,
spec
,
k8s_resources
):
self
.
_pod_spec
=
(
spec
,
k8s_resources
)
def
status_get
(
self
,
*
,
is_app
=
False
):
if
is_app
:
return
self
.
_app_status
else
:
return
self
.
_unit_status
def
status_set
(
self
,
status
,
message
=
''
,
*
,
is_app
=
False
):
if
is_app
:
self
.
_app_status
=
(
status
,
message
)
else
:
self
.
_unit_status
=
(
status
,
message
)
def
storage_list
(
self
,
name
):
raise
NotImplementedError
(
self
.
storage_list
)
def
storage_get
(
self
,
storage_name_id
,
attribute
):
raise
NotImplementedError
(
self
.
storage_get
)
def
storage_add
(
self
,
name
,
count
=
1
):
raise
NotImplementedError
(
self
.
storage_add
)
def
action_get
(
self
):
raise
NotImplementedError
(
self
.
action_get
)
def
action_set
(
self
,
results
):
raise
NotImplementedError
(
self
.
action_set
)
def
action_log
(
self
,
message
):
raise
NotImplementedError
(
self
.
action_log
)
def
action_fail
(
self
,
message
=
''
):
raise
NotImplementedError
(
self
.
action_fail
)
def
network_get
(
self
,
endpoint_name
,
relation_id
=
None
):
raise
NotImplementedError
(
self
.
network_get
)
magma/squid_cnf/charms/squid/mod/operator/requirements.txt
deleted
100644 → 0
View file @
e2197769
PyYAML
magma/squid_cnf/charms/squid/mod/operator/run_tests
deleted
100755 → 0
View file @
e2197769
#!/bin/bash
python3
-m
unittest
"
$@
"
magma/squid_cnf/charms/squid/mod/operator/setup.py
deleted
100644 → 0
View file @
e2197769
# Copyright 2019 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from
setuptools
import
setup
with
open
(
"README.md"
,
"r"
)
as
fh
:
long_description
=
fh
.
read
()
setup
(
name
=
"ops"
,
version
=
"0.0.1"
,
description
=
"The Python library behind great charms"
,
long_description
=
long_description
,
long_description_content_type
=
"text/markdown"
,
license
=
"Apache-2.0"
,
url
=
"https://github.com/canonical/operator"
,
packages
=
[
"ops"
],
classifiers
=
[
"Development Status :: 4 - Beta"
,
"Programming Language :: Python :: 3"
,
"Programming Language :: Python :: 3.6"
,
"Programming Language :: Python :: 3.7"
,
"Programming Language :: Python :: 3.8"
,
"License :: OSI Approved :: Apache Software License"
,
],
)
magma/squid_cnf/charms/squid/mod/operator/test/__init__.py
deleted
100644 → 0
View file @
e2197769
magma/squid_cnf/charms/squid/mod/operator/test/bin/relation-ids
deleted
100755 → 0
View file @
e2197769
#!/bin/bash
case
$1
in
db
)
echo
'["db:1"]'
;;
mon
)
echo
'["mon:2"]'
;;
ha
)
echo
'[]'
;;
db0
)
echo
'[]'
;;
db1
)
echo
'["db1:4"]'
;;
db2
)
echo
'["db2:5", "db2:6"]'
;;
*
)
echo
'[]'
;;
esac
magma/squid_cnf/charms/squid/mod/operator/test/bin/relation-list
deleted
100755 → 0
View file @
e2197769
#!/bin/bash
fail_not_found
()
{
1>&2
echo
"ERROR invalid value
\"
$1
\"
for option -r: relation not found"
exit
2
}
case
$2
in
1
)
echo
'["remote/0"]'
;;
2
)
echo
'["remote/0"]'
;;
3
)
fail_not_found
$2
;;
4
)
echo
'["remoteapp1/0"]'
;;
5
)
echo
'["remoteapp1/0"]'
;;
6
)
echo
'["remoteapp2/0"]'
;;
*
)
fail_not_found
$2
;;
esac
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/config.yaml
deleted
100644 → 0
View file @
e2197769
"
options"
:
{}
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/lib/__init__.py
deleted
100755 → 0
View file @
e2197769
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/lib/ops
deleted
120000 → 0
View file @
e2197769
../../../../ops
\ No newline at end of file
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/metadata.yaml
deleted
100644 → 0
View file @
e2197769
name
:
main
summary
:
A charm used for testing the basic operation of the entrypoint code.
maintainer
:
Dmitrii Shcherbakov <dmitrii.shcherbakov@canonical.com>
description
:
A charm used for testing the basic operation of the entrypoint code.
tags
:
-
misc
series
:
-
bionic
-
cosmic
-
disco
min-juju-version
:
2.7.1
provides
:
db
:
interface
:
db
requires
:
mon
:
interface
:
monitoring
peers
:
ha
:
interface
:
cluster
subordinate
:
false
storage
:
disks
:
type
:
block
multiple
:
range
:
0-
magma/squid_cnf/charms/squid/mod/operator/test/charms/test_main/src/charm.py
deleted
100755 → 0
View file @
e2197769
#!/usr/bin/env python3
# Copyright 2019 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import
os
import
base64
import
pickle
import
sys
import
logging
sys
.
path
.
append
(
'lib'
)
from
ops.charm
import
CharmBase
# noqa: E402 (module-level import after non-import code)
from
ops.main
import
main
# noqa: E402 (ditto)
logger
=
logging
.
getLogger
()
class
Charm
(
CharmBase
):
def
__init__
(
self
,
*
args
):
super
().
__init__
(
*
args
)
# This environment variable controls the test charm behavior.
charm_config
=
os
.
environ
.
get
(
'CHARM_CONFIG'
)
if
charm_config
is
not
None
:
self
.
_charm_config
=
pickle
.
loads
(
base64
.
b64decode
(
charm_config
))
else
:
self
.
_charm_config
=
{}
# TODO: refactor to use StoredState
# (this implies refactoring most of test_main.py)
self
.
_state_file
=
self
.
_charm_config
.
get
(
'STATE_FILE'
)
try
:
with
open
(
str
(
self
.
_state_file
),
'rb'
)
as
f
:
self
.
_state
=
pickle
.
load
(
f
)
except
(
FileNotFoundError
,
EOFError
):
self
.
_state
=
{
'on_install'
:
[],
'on_start'
:
[],
'on_config_changed'
:
[],
'on_update_status'
:
[],
'on_leader_settings_changed'
:
[],
'on_db_relation_joined'
:
[],
'on_mon_relation_changed'
:
[],
'on_mon_relation_departed'
:
[],
'on_ha_relation_broken'
:
[],
'on_foo_bar_action'
:
[],
'on_start_action'
:
[],
'on_collect_metrics'
:
[],
'on_log_critical_action'
:
[],
'on_log_error_action'
:
[],
'on_log_warning_action'
:
[],
'on_log_info_action'
:
[],
'on_log_debug_action'
:
[],
# Observed event types per invocation. A list is used to preserve the
# order in which charm handlers have observed the events.
'observed_event_types'
:
[],
}
self
.
framework
.
observe
(
self
.
on
.
install
,
self
)
self
.
framework
.
observe
(
self
.
on
.
start
,
self
)
self
.
framework
.
observe
(
self
.
on
.
config_changed
,
self
)
self
.
framework
.
observe
(
self
.
on
.
update_status
,
self
)
self
.
framework
.
observe
(
self
.
on
.
leader_settings_changed
,
self
)
# Test relation events with endpoints from different
# sections (provides, requires, peers) as well.
self
.
framework
.
observe
(
self
.
on
.
db_relation_joined
,
self
)
self
.
framework
.
observe
(
self
.
on
.
mon_relation_changed
,
self
)
self
.
framework
.
observe
(
self
.
on
.
mon_relation_departed
,
self
)
self
.
framework
.
observe
(
self
.
on
.
ha_relation_broken
,
self
)
if
self
.
_charm_config
.
get
(
'USE_ACTIONS'
):
self
.
framework
.
observe
(
self
.
on
.
start_action
,
self
)
self
.
framework
.
observe
(
self
.
on
.
foo_bar_action
,
self
)
self
.
framework
.
observe
(
self
.
on
.
collect_metrics
,
self
)
if
self
.
_charm_config
.
get
(
'USE_LOG_ACTIONS'
):
self
.
framework
.
observe
(
self
.
on
.
log_critical_action
,
self
)
self
.
framework
.
observe
(
self
.
on
.
log_error_action
,
self
)
self
.
framework
.
observe
(
self
.
on
.
log_warning_action
,
self
)
self
.
framework
.
observe
(
self
.
on
.
log_info_action
,
self
)
self
.
framework
.
observe
(
self
.
on
.
log_debug_action
,
self
)
def
_write_state
(
self
):
"""Write state variables so that the parent process can read them.
Each invocation will override the previous state which is intentional.
"""
if
self
.
_state_file
is
not
None
:
with
self
.
_state_file
.
open
(
'wb'
)
as
f
:
pickle
.
dump
(
self
.
_state
,
f
)
def
on_install
(
self
,
event
):
self
.
_state
[
'on_install'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_write_state
()
def
on_start
(
self
,
event
):
self
.
_state
[
'on_start'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_write_state
()
def
on_config_changed
(
self
,
event
):
self
.
_state
[
'on_config_changed'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
event
.
defer
()
self
.
_write_state
()
def
on_update_status
(
self
,
event
):
self
.
_state
[
'on_update_status'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_write_state
()
def
on_leader_settings_changed
(
self
,
event
):
self
.
_state
[
'on_leader_settings_changed'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_write_state
()
def
on_db_relation_joined
(
self
,
event
):
assert
event
.
app
is
not
None
,
'application name cannot be None for a relation-joined event'
self
.
_state
[
'on_db_relation_joined'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_state
[
'db_relation_joined_data'
]
=
event
.
snapshot
()
self
.
_write_state
()
def
on_mon_relation_changed
(
self
,
event
):
assert
event
.
app
is
not
None
,
(
'application name cannot be None for a relation-changed event'
)
if
os
.
environ
.
get
(
'JUJU_REMOTE_UNIT'
):
assert
event
.
unit
is
not
None
,
(
'a unit name cannot be None for a relation-changed event'
' associated with a remote unit'
)
self
.
_state
[
'on_mon_relation_changed'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_state
[
'mon_relation_changed_data'
]
=
event
.
snapshot
()
self
.
_write_state
()
def
on_mon_relation_departed
(
self
,
event
):
assert
event
.
app
is
not
None
,
(
'application name cannot be None for a relation-departed event'
)
self
.
_state
[
'on_mon_relation_departed'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_state
[
'mon_relation_departed_data'
]
=
event
.
snapshot
()
self
.
_write_state
()
def
on_ha_relation_broken
(
self
,
event
):
assert
event
.
app
is
None
,
(
'relation-broken events cannot have a reference to a remote application'
)
assert
event
.
unit
is
None
,
(
'relation broken events cannot have a reference to a remote unit'
)
self
.
_state
[
'on_ha_relation_broken'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_state
[
'ha_relation_broken_data'
]
=
event
.
snapshot
()
self
.
_write_state
()
def
on_start_action
(
self
,
event
):
assert
event
.
handle
.
kind
==
'start_action'
,
(
'event action name cannot be different from the one being handled'
)
self
.
_state
[
'on_start_action'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_write_state
()
def
on_foo_bar_action
(
self
,
event
):
assert
event
.
handle
.
kind
==
'foo_bar_action'
,
(
'event action name cannot be different from the one being handled'
)
self
.
_state
[
'on_foo_bar_action'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
self
.
_write_state
()
def
on_collect_metrics
(
self
,
event
):
self
.
_state
[
'on_collect_metrics'
].
append
(
type
(
event
))
self
.
_state
[
'observed_event_types'
].
append
(
type
(
event
))
event
.
add_metrics
({
'foo'
:
42
},
{
'bar'
:
4.2
})
self
.
_write_state
()
def
on_log_critical_action
(
self
,
event
):
logger
.
critical
(
'super critical'
)
def
on_log_error_action
(
self
,
event
):
logger
.
error
(
'grave error'
)
def
on_log_warning_action
(
self
,
event
):
logger
.
warning
(
'wise warning'
)
def
on_log_info_action
(
self
,
event
):
logger
.
info
(
'useful info'
)
def
on_log_debug_action
(
self
,
event
):
logger
.
debug
(
'insightful debug'
)
if
__name__
==
'__main__'
:
main
(
Charm
)
magma/squid_cnf/charms/squid/mod/operator/test/test_charm.py
deleted
100755 → 0
View file @
e2197769
#!/usr/bin/python3
# Copyright 2019 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import
os
import
unittest
import
tempfile
import
shutil
from
pathlib
import
Path
from
ops.charm
import
(
CharmBase
,
CharmMeta
,
CharmEvents
,
)
from
ops.framework
import
Framework
,
EventSource
,
EventBase
from
ops.model
import
Model
,
ModelBackend
from
.test_helpers
import
fake_script
,
fake_script_calls
class
TestCharm
(
unittest
.
TestCase
):
def
setUp
(
self
):
def
restore_env
(
env
):
os
.
environ
.
clear
()
os
.
environ
.
update
(
env
)
self
.
addCleanup
(
restore_env
,
os
.
environ
.
copy
())
os
.
environ
[
'PATH'
]
=
"{}:{}"
.
format
(
Path
(
__file__
).
parent
/
'bin'
,
os
.
environ
[
'PATH'
])
os
.
environ
[
'JUJU_UNIT_NAME'
]
=
'local/0'
self
.
tmpdir
=
Path
(
tempfile
.
mkdtemp
())
self
.
addCleanup
(
shutil
.
rmtree
,
str
(
self
.
tmpdir
))
self
.
meta
=
CharmMeta
()
class
CustomEvent
(
EventBase
):
pass
class
TestCharmEvents
(
CharmEvents
):
custom
=
EventSource
(
CustomEvent
)
# Relations events are defined dynamically and modify the class attributes.
# We use a subclass temporarily to prevent these side effects from leaking.
CharmBase
.
on
=
TestCharmEvents
()
def
cleanup
():
CharmBase
.
on
=
CharmEvents
()
self
.
addCleanup
(
cleanup
)
def
create_framework
(
self
):
model
=
Model
(
'local/0'
,
self
.
meta
,
ModelBackend
())
framework
=
Framework
(
self
.
tmpdir
/
"framework.data"
,
self
.
tmpdir
,
self
.
meta
,
model
)
self
.
addCleanup
(
framework
.
close
)
return
framework
def
test_basic
(
self
):
class
MyCharm
(
CharmBase
):
def
__init__
(
self
,
*
args
):
super
().
__init__
(
*
args
)
self
.
started
=
False
framework
.
observe
(
self
.
on
.
start
,
self
)
def
on_start
(
self
,
event
):
self
.
started
=
True
events
=
list
(
MyCharm
.
on
.
events
())
self
.
assertIn
(
'install'
,
events
)
self
.
assertIn
(
'custom'
,
events
)
framework
=
self
.
create_framework
()
charm
=
MyCharm
(
framework
,
None
)
charm
.
on
.
start
.
emit
()
self
.
assertEqual
(
charm
.
started
,
True
)
def
test_helper_properties
(
self
):
framework
=
self
.
create_framework
()
class
MyCharm
(
CharmBase
):
pass
charm
=
MyCharm
(
framework
,
None
)
self
.
assertEqual
(
charm
.
app
,
framework
.
model
.
app
)
self
.
assertEqual
(
charm
.
unit
,
framework
.
model
.
unit
)
self
.
assertEqual
(
charm
.
meta
,
framework
.
meta
)
self
.
assertEqual
(
charm
.
charm_dir
,
framework
.
charm_dir
)
def
test_relation_events
(
self
):
class
MyCharm
(
CharmBase
):
def
__init__
(
self
,
*
args
):
super
().
__init__
(
*
args
)
self
.
seen
=
[]
for
rel
in
(
'req1'
,
'req-2'
,
'pro1'
,
'pro-2'
,
'peer1'
,
'peer-2'
):
# Hook up relation events to generic handler.
self
.
framework
.
observe
(
self
.
on
[
rel
].
relation_joined
,
self
.
on_any_relation
)
self
.
framework
.
observe
(
self
.
on
[
rel
].
relation_changed
,
self
.
on_any_relation
)
self
.
framework
.
observe
(
self
.
on
[
rel
].
relation_departed
,
self
.
on_any_relation
)
self
.
framework
.
observe
(
self
.
on
[
rel
].
relation_broken
,
self
.
on_any_relation
)
def
on_any_relation
(
self
,
event
):
assert
event
.
relation
.
name
==
'req1'
assert
event
.
relation
.
app
.
name
==
'remote'
self
.
seen
.
append
(
type
(
event
).
__name__
)
# language=YAML
self
.
meta
=
CharmMeta
.
from_yaml
(
metadata
=
'''
name: my-charm
requires:
req1:
interface: req1
req-2:
interface: req2
provides:
pro1:
interface: pro1
pro-2:
interface: pro2
peers:
peer1:
interface: peer1
peer-2:
interface: peer2
'''
)
charm
=
MyCharm
(
self
.
create_framework
(),
None
)
rel
=
charm
.
framework
.
model
.
get_relation
(
'req1'
,
1
)
unit
=
charm
.
framework
.
model
.
get_unit
(
'remote/0'
)
charm
.
on
[
'req1'
].
relation_joined
.
emit
(
rel
,
unit
)
charm
.
on
[
'req1'
].
relation_changed
.
emit
(
rel
,
unit
)
charm
.
on
[
'req-2'
].
relation_changed
.
emit
(
rel
,
unit
)
charm
.
on
[
'pro1'
].
relation_departed
.
emit
(
rel
,
unit
)
charm
.
on
[
'pro-2'
].
relation_departed
.
emit
(
rel
,
unit
)
charm
.
on
[
'peer1'
].
relation_broken
.
emit
(
rel
)
charm
.
on
[
'peer-2'
].
relation_broken
.
emit
(
rel
)
self
.
assertEqual
(
charm
.
seen
,
[
'RelationJoinedEvent'
,
'RelationChangedEvent'
,
'RelationChangedEvent'
,
'RelationDepartedEvent'
,
'RelationDepartedEvent'
,
'RelationBrokenEvent'
,
'RelationBrokenEvent'
,
])
def
test_storage_events
(
self
):
class
MyCharm
(
CharmBase
):
def
__init__
(
self
,
*
args
):
super
().
__init__
(
*
args
)
self
.
seen
=
[]
self
.
framework
.
observe
(
self
.
on
[
'stor1'
].
storage_attached
,
self
)
self
.
framework
.
observe
(
self
.
on
[
'stor2'
].
storage_detaching
,
self
)
self
.
framework
.
observe
(
self
.
on
[
'stor3'
].
storage_attached
,
self
)
self
.
framework
.
observe
(
self
.
on
[
'stor-4'
].
storage_attached
,
self
)
def
on_stor1_storage_attached
(
self
,
event
):
self
.
seen
.
append
(
type
(
event
).
__name__
)
def
on_stor2_storage_detaching
(
self
,
event
):
self
.
seen
.
append
(
type
(
event
).
__name__
)
def
on_stor3_storage_attached
(
self
,
event
):
self
.
seen
.
append
(
type
(
event
).
__name__
)
def
on_stor_4_storage_attached
(
self
,
event
):
self
.
seen
.
append
(
type
(
event
).
__name__
)
# language=YAML
self
.
meta
=
CharmMeta
.
from_yaml
(
'''
name: my-charm
storage:
stor-4:
multiple:
range: 2-4
type: filesystem
stor1:
type: filesystem
stor2:
multiple:
range: "2"
type: filesystem
stor3:
multiple:
range: 2-
type: filesystem
'''
)
self
.
assertIsNone
(
self
.
meta
.
storages
[
'stor1'
].
multiple_range
)
self
.
assertEqual
(
self
.
meta
.
storages
[
'stor2'
].
multiple_range
,
(
2
,
2
))
self
.
assertEqual
(
self
.
meta
.
storages
[
'stor3'
].
multiple_range
,
(
2
,
None
))
self
.
assertEqual
(
self
.
meta
.
storages
[
'stor-4'
].
multiple_range
,
(
2
,
4
))
charm
=
MyCharm
(
self
.
create_framework
(),
None
)
charm
.
on
[
'stor1'
].
storage_attached
.
emit
()
charm
.
on
[
'stor2'
].
storage_detaching
.
emit
()
charm
.
on
[
'stor3'
].
storage_attached
.
emit
()
charm
.
on
[
'stor-4'
].
storage_attached
.
emit
()
self
.
assertEqual
(
charm
.
seen
,
[
'StorageAttachedEvent'
,
'StorageDetachingEvent'
,
'StorageAttachedEvent'
,
'StorageAttachedEvent'
,
])
@
classmethod
def
_get_action_test_meta
(
cls
):
# language=YAML
return
CharmMeta
.
from_yaml
(
metadata
=
'''
name: my-charm
'''
,
actions
=
'''
foo-bar:
description: "Foos the bar."
params:
foo-name:
description: "A foo name to bar"
type: string
silent:
default: false
description: ""
type: boolean
required: foo-bar
title: foo-bar
start:
description: "Start the unit."
'''
)
def
_test_action_events
(
self
,
cmd_type
):
class
MyCharm
(
CharmBase
):
def
__init__
(
self
,
*
args
):
super
().
__init__
(
*
args
)
framework
.
observe
(
self
.
on
.
foo_bar_action
,
self
)
framework
.
observe
(
self
.
on
.
start_action
,
self
)
def
on_foo_bar_action
(
self
,
event
):
self
.
seen_action_params
=
event
.
params
event
.
log
(
'test-log'
)
event
.
set_results
({
'res'
:
'val with spaces'
})
event
.
fail
(
'test-fail'
)
def
on_start_action
(
self
,
event
):
pass
fake_script
(
self
,
cmd_type
+
'-get'
,
"""echo '{"foo-name": "name", "silent": true}'"""
)
fake_script
(
self
,
cmd_type
+
'-set'
,
""
)
fake_script
(
self
,
cmd_type
+
'-log'
,
""
)
fake_script
(
self
,
cmd_type
+
'-fail'
,
""
)
self
.
meta
=
self
.
_get_action_test_meta
()
os
.
environ
[
'JUJU_{}_NAME'
.
format
(
cmd_type
.
upper
())]
=
'foo-bar'
framework
=
self
.
create_framework
()
charm
=
MyCharm
(
framework
,
None
)
events
=
list
(
MyCharm
.
on
.
events
())
self
.
assertIn
(
'foo_bar_action'
,
events
)
self
.
assertIn
(
'start_action'
,
events
)
charm
.
on
.
foo_bar_action
.
emit
()
self
.
assertEqual
(
charm
.
seen_action_params
,
{
"foo-name"
:
"name"
,
"silent"
:
True
})
self
.
assertEqual
(
fake_script_calls
(
self
),
[
[
cmd_type
+
'-get'
,
'--format=json'
],
[
cmd_type
+
'-log'
,
"test-log"
],
[
cmd_type
+
'-set'
,
"res=val with spaces"
],
[
cmd_type
+
'-fail'
,
"test-fail"
],
])
# Make sure that action events that do not match the current context are
# not possible to emit by hand.
with
self
.
assertRaises
(
RuntimeError
):
charm
.
on
.
start_action
.
emit
()
def
test_action_events
(
self
):
self
.
_test_action_events
(
'action'
)
def
_test_action_event_defer_fails
(
self
,
cmd_type
):
class
MyCharm
(
CharmBase
):
def
__init__
(
self
,
*
args
):
super
().
__init__
(
*
args
)
framework
.
observe
(
self
.
on
.
start_action
,
self
)
def
on_start_action
(
self
,
event
):
event
.
defer
()
fake_script
(
self
,
cmd_type
+
'-get'
,
"""echo '{"foo-name": "name", "silent": true}'"""
)
self
.
meta
=
self
.
_get_action_test_meta
()
os
.
environ
[
'JUJU_{}_NAME'
.
format
(
cmd_type
.
upper
())]
=
'start'
framework
=
self
.
create_framework
()
charm
=
MyCharm
(
framework
,
None
)
with
self
.
assertRaises
(
RuntimeError
):
charm
.
on
.
start_action
.
emit
()
def
test_action_event_defer_fails
(
self
):
self
.
_test_action_event_defer_fails
(
'action'
)
if
__name__
==
"__main__"
:
unittest
.
main
()
magma/squid_cnf/charms/squid/mod/operator/test/test_framework.py
deleted
100644 → 0
View file @
e2197769
This diff is collapsed.
Click to expand it.
magma/squid_cnf/charms/squid/mod/operator/test/test_helpers.py
deleted
100644 → 0
View file @
e2197769
# Copyright 2019 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import
os
import
pathlib
import
subprocess
import
shutil
import
tempfile
import
unittest
def
fake_script
(
test_case
,
name
,
content
):
if
not
hasattr
(
test_case
,
'fake_script_path'
):
fake_script_path
=
tempfile
.
mkdtemp
(
'-fake_script'
)
os
.
environ
[
'PATH'
]
=
'{}:{}'
.
format
(
fake_script_path
,
os
.
environ
[
"PATH"
])
def
cleanup
():
shutil
.
rmtree
(
fake_script_path
)
os
.
environ
[
'PATH'
]
=
os
.
environ
[
'PATH'
].
replace
(
fake_script_path
+
':'
,
''
)
test_case
.
addCleanup
(
cleanup
)
test_case
.
fake_script_path
=
pathlib
.
Path
(
fake_script_path
)
with
(
test_case
.
fake_script_path
/
name
).
open
(
'wt'
)
as
f
:
# Before executing the provided script, dump the provided arguments in calls.txt.
f
.
write
(
'''#!/bin/bash
{ echo -n $(basename $0); printf ";%s" "$@"; echo; } >> $(dirname $0)/calls.txt
'''
+
content
)
os
.
chmod
(
str
(
test_case
.
fake_script_path
/
name
),
0o755
)
def
fake_script_calls
(
test_case
,
clear
=
False
):
try
:
with
(
test_case
.
fake_script_path
/
'calls.txt'
).
open
(
'r+t'
)
as
f
:
calls
=
[
line
.
split
(
';'
)
for
line
in
f
.
read
().
splitlines
()]
if
clear
:
f
.
truncate
(
0
)
return
calls
except
FileNotFoundError
:
return
[]
class
FakeScriptTest
(
unittest
.
TestCase
):
def
test_fake_script_works
(
self
):
fake_script
(
self
,
'foo'
,
'echo foo runs'
)
fake_script
(
self
,
'bar'
,
'echo bar runs'
)
output
=
subprocess
.
getoutput
(
'foo a "b c "; bar "d e" f'
)
self
.
assertEqual
(
output
,
'foo runs
\n
bar runs'
)
self
.
assertEqual
(
fake_script_calls
(
self
),
[
[
'foo'
,
'a'
,
'b c '
],
[
'bar'
,
'd e'
,
'f'
],
])
def
test_fake_script_clear
(
self
):
fake_script
(
self
,
'foo'
,
'echo foo runs'
)
output
=
subprocess
.
getoutput
(
'foo a "b c"'
)
self
.
assertEqual
(
output
,
'foo runs'
)
self
.
assertEqual
(
fake_script_calls
(
self
,
clear
=
True
),
[[
'foo'
,
'a'
,
'b c'
]])
fake_script
(
self
,
'bar'
,
'echo bar runs'
)
output
=
subprocess
.
getoutput
(
'bar "d e" f'
)
self
.
assertEqual
(
output
,
'bar runs'
)
self
.
assertEqual
(
fake_script_calls
(
self
,
clear
=
True
),
[[
'bar'
,
'd e'
,
'f'
]])
self
.
assertEqual
(
fake_script_calls
(
self
,
clear
=
True
),
[])
Prev
1
2
3
4
5
6
…
8
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment