First cut
This commit is contained in:
9
charms/glance-k8s/.flake8
Normal file
9
charms/glance-k8s/.flake8
Normal file
@@ -0,0 +1,9 @@
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
select: E,W,F,C,N
|
||||
exclude:
|
||||
venv
|
||||
.git
|
||||
build
|
||||
dist
|
||||
*.egg_info
|
8
charms/glance-k8s/.gitignore
vendored
Normal file
8
charms/glance-k8s/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
venv/
|
||||
build/
|
||||
.stestr/
|
||||
*.charm
|
||||
.tox
|
||||
.coverage
|
||||
__pycache__/
|
||||
*.py[cod]
|
3
charms/glance-k8s/.jujuignore
Normal file
3
charms/glance-k8s/.jujuignore
Normal file
@@ -0,0 +1,3 @@
|
||||
/venv
|
||||
*.py[cod]
|
||||
*.charm
|
3
charms/glance-k8s/.stestr.conf
Normal file
3
charms/glance-k8s/.stestr.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
test_path=./unit_tests
|
||||
top_dir=./
|
34
charms/glance-k8s/CONTRIBUTING.md
Normal file
34
charms/glance-k8s/CONTRIBUTING.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# sunbeam-glance-operator
|
||||
|
||||
## Developing
|
||||
|
||||
Create and activate a virtualenv with the development requirements:
|
||||
|
||||
virtualenv -p python3 venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
## Code overview
|
||||
|
||||
TEMPLATE-TODO:
|
||||
One of the most important things a consumer of your charm (or library)
|
||||
needs to know is what set of functionality it provides. Which categories
|
||||
does it fit into? Which events do you listen to? Which libraries do you
|
||||
consume? Which ones do you export and how are they used?
|
||||
|
||||
## Intended use case
|
||||
|
||||
TEMPLATE-TODO:
|
||||
Why were these decisions made? What's the scope of your charm?
|
||||
|
||||
## Roadmap
|
||||
|
||||
If this Charm doesn't fulfill all of the initial functionality you were
|
||||
hoping for or planning on, please add a Roadmap or TODO here
|
||||
|
||||
## Testing
|
||||
|
||||
The Python operator framework includes a very nice harness for testing
|
||||
operator behaviour without full deployment. Just `run_tests`:
|
||||
|
||||
./run_tests
|
202
charms/glance-k8s/LICENSE
Normal file
202
charms/glance-k8s/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
24
charms/glance-k8s/README.md
Normal file
24
charms/glance-k8s/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# sunbeam-glance-operator
|
||||
|
||||
## Description
|
||||
|
||||
TODO: Describe your charm in a few paragraphs of Markdown
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Provide high-level usage, such as required config or relations
|
||||
|
||||
|
||||
## Relations
|
||||
|
||||
TODO: Provide any relations which are provided or required by your charm
|
||||
|
||||
## OCI Images
|
||||
|
||||
TODO: Include a link to the default image your charm uses
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines
|
||||
on enhancements to this charm following best practice guidelines, and
|
||||
`CONTRIBUTING.md` for developer guidance.
|
16
charms/glance-k8s/actions.yaml
Normal file
16
charms/glance-k8s/actions.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright 2021 liam
|
||||
# See LICENSE file for licensing details.
|
||||
#
|
||||
# TEMPLATE-TODO: change this example to suit your needs.
|
||||
# If you don't need actions, you can remove the file entirely.
|
||||
# It ties in to the example _on_fortune_action handler in src/charm.py
|
||||
#
|
||||
# Learn more about actions at: https://juju.is/docs/sdk/actions
|
||||
|
||||
fortune:
|
||||
description: Returns a pithy phrase.
|
||||
params:
|
||||
fail:
|
||||
description: "Fail with this message"
|
||||
type: string
|
||||
default: ""
|
10
charms/glance-k8s/charmcraft.yaml
Normal file
10
charms/glance-k8s/charmcraft.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# Learn more about charmcraft.yaml configuration at:
|
||||
# https://juju.is/docs/sdk/charmcraft-config
|
||||
type: "charm"
|
||||
bases:
|
||||
- build-on:
|
||||
- name: "ubuntu"
|
||||
channel: "20.04"
|
||||
run-on:
|
||||
- name: "ubuntu"
|
||||
channel: "20.04"
|
257
charms/glance-k8s/config.yaml
Normal file
257
charms/glance-k8s/config.yaml
Normal file
@@ -0,0 +1,257 @@
|
||||
options:
|
||||
debug:
|
||||
default: False
|
||||
description: Enable debug logging.
|
||||
type: boolean
|
||||
os-admin-hostname:
|
||||
default: glance.juju
|
||||
description: |
|
||||
The hostname or address of the admin endpoints that should be advertised
|
||||
in the glance image provider.
|
||||
type: string
|
||||
os-internal-hostname:
|
||||
default: glance.juju
|
||||
description: |
|
||||
The hostname or address of the internal endpoints that should be advertised
|
||||
in the glance image provider.
|
||||
type: string
|
||||
os-public-hostname:
|
||||
default: glance.juju
|
||||
description: |
|
||||
The hostname or address of the internal endpoints that should be advertised
|
||||
in the glance image provider.
|
||||
type: string
|
||||
region:
|
||||
default: RegionOne
|
||||
description: Space delimited list of OpenStack regions
|
||||
type: string
|
||||
ceph-osd-replication-count:
|
||||
default: 3
|
||||
type: int
|
||||
description: |
|
||||
This value dictates the number of replicas ceph must make of any
|
||||
object it stores within the cinder rbd pool. Of course, this only
|
||||
applies if using Ceph as a backend store. Note that once the cinder
|
||||
rbd pool has been created, changing this value will not have any
|
||||
effect (although it can be changed in ceph by manually configuring
|
||||
your ceph cluster).
|
||||
ceph-pool-weight:
|
||||
type: int
|
||||
default: 40
|
||||
description: |
|
||||
Defines a relative weighting of the pool as a percentage of the total
|
||||
amount of data in the Ceph cluster. This effectively weights the number
|
||||
of placement groups for the pool created to be appropriately portioned
|
||||
to the amount of data expected. For example, if the ephemeral volumes
|
||||
for the OpenStack compute instances are expected to take up 20% of the
|
||||
overall configuration then this value would be specified as 20. Note -
|
||||
it is important to choose an appropriate value for the pool weight as
|
||||
this directly affects the number of placement groups which will be
|
||||
created for the pool. The number of placement groups for a pool can
|
||||
only be increased, never decreased - so it is important to identify the
|
||||
percent of data that will likely reside in the pool.
|
||||
volume-backend-name:
|
||||
default:
|
||||
type: string
|
||||
description: |
|
||||
Volume backend name for the backend. The default value is the
|
||||
application name in the Juju model, e.g. "cinder-ceph-mybackend"
|
||||
if it's deployed as `juju deploy cinder-ceph cinder-ceph-mybackend`.
|
||||
A common backend name can be set to multiple backends with the
|
||||
same characters so that those can be treated as a single virtual
|
||||
backend associated with a single volume type.
|
||||
backend-availability-zone:
|
||||
default:
|
||||
type: string
|
||||
description: |
|
||||
Availability zone name of this volume backend. If set, it will
|
||||
override the default availability zone. Supported for Pike or
|
||||
newer releases.
|
||||
restrict-ceph-pools:
|
||||
default: False
|
||||
type: boolean
|
||||
description: |
|
||||
Optionally restrict Ceph key permissions to access pools as required.
|
||||
rbd-pool-name:
|
||||
default:
|
||||
type: string
|
||||
description: |
|
||||
Optionally specify an existing rbd pool that cinder should map to.
|
||||
rbd-flatten-volume-from-snapshot:
|
||||
default:
|
||||
type: boolean
|
||||
default: False
|
||||
description: |
|
||||
Flatten volumes created from snapshots to remove dependency from
|
||||
volume to snapshot. Supported on Queens+
|
||||
rbd-mirroring-mode:
|
||||
type: string
|
||||
default: pool
|
||||
description: |
|
||||
The RBD mirroring mode used for the Ceph pool. This option is only used
|
||||
with 'replicated' pool type, as it's not supported for 'erasure-coded'
|
||||
pool type - valid values: 'pool' and 'image'
|
||||
pool-type:
|
||||
type: string
|
||||
default: replicated
|
||||
description: |
|
||||
Ceph pool type to use for storage - valid values include ‘replicated’
|
||||
and ‘erasure-coded’.
|
||||
ec-profile-name:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
Name for the EC profile to be created for the EC pools. If not defined
|
||||
a profile name will be generated based on the name of the pool used by
|
||||
the application.
|
||||
ec-rbd-metadata-pool:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
Name of the metadata pool to be created (for RBD use-cases). If not
|
||||
defined a metadata pool name will be generated based on the name of
|
||||
the data pool used by the application. The metadata pool is always
|
||||
replicated, not erasure coded.
|
||||
ec-profile-k:
|
||||
type: int
|
||||
default: 1
|
||||
description: |
|
||||
Number of data chunks that will be used for EC data pool. K+M factors
|
||||
should never be greater than the number of available zones (or hosts)
|
||||
for balancing.
|
||||
ec-profile-m:
|
||||
type: int
|
||||
default: 2
|
||||
description: |
|
||||
Number of coding chunks that will be used for EC data pool. K+M factors
|
||||
should never be greater than the number of available zones (or hosts)
|
||||
for balancing.
|
||||
ec-profile-locality:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
(lrc plugin - l) Group the coding and data chunks into sets of size l.
|
||||
For instance, for k=4 and m=2, when l=3 two groups of three are created.
|
||||
Each set can be recovered without reading chunks from another set. Note
|
||||
that using the lrc plugin does incur more raw storage usage than isa or
|
||||
jerasure in order to reduce the cost of recovery operations.
|
||||
ec-profile-crush-locality:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
(lrc plugin) The type of the crush bucket in which each set of chunks
|
||||
defined by l will be stored. For instance, if it is set to rack, each
|
||||
group of l chunks will be placed in a different rack. It is used to
|
||||
create a CRUSH rule step such as step choose rack. If it is not set,
|
||||
no such grouping is done.
|
||||
ec-profile-durability-estimator:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
(shec plugin - c) The number of parity chunks each of which includes
|
||||
each data chunk in its calculation range. The number is used as a
|
||||
durability estimator. For instance, if c=2, 2 OSDs can be down
|
||||
without losing data.
|
||||
ec-profile-helper-chunks:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
(clay plugin - d) Number of OSDs requested to send data during
|
||||
recovery of a single chunk. d needs to be chosen such that
|
||||
k+1 <= d <= k+m-1. Larger the d, the better the savings.
|
||||
ec-profile-scalar-mds:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
(clay plugin) specifies the plugin that is used as a building
|
||||
block in the layered construction. It can be one of jerasure,
|
||||
isa, shec (defaults to jerasure).
|
||||
ec-profile-plugin:
|
||||
type: string
|
||||
default: jerasure
|
||||
description: |
|
||||
EC plugin to use for this applications pool. The following list of
|
||||
plugins acceptable - jerasure, lrc, isa, shec, clay.
|
||||
ec-profile-technique:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
EC profile technique used for this applications pool - will be
|
||||
validated based on the plugin configured via ec-profile-plugin.
|
||||
Supported techniques are ‘reed_sol_van’, ‘reed_sol_r6_op’,
|
||||
‘cauchy_orig’, ‘cauchy_good’, ‘liber8tion’ for jerasure,
|
||||
‘reed_sol_van’, ‘cauchy’ for isa and ‘single’, ‘multiple’
|
||||
for shec.
|
||||
ec-profile-device-class:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
Device class from CRUSH map to use for placement groups for
|
||||
erasure profile - valid values: ssd, hdd or nvme (or leave
|
||||
unset to not use a device class).
|
||||
bluestore-compression-algorithm:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
Compressor to use (if any) for pools requested by this charm.
|
||||
.
|
||||
NOTE: The ceph-osd charm sets a global default for this value (defaults
|
||||
to 'lz4' unless configured by the end user) which will be used unless
|
||||
specified for individual pools.
|
||||
bluestore-compression-mode:
|
||||
type: string
|
||||
default:
|
||||
description: |
|
||||
Policy for using compression on pools requested by this charm.
|
||||
.
|
||||
'none' means never use compression.
|
||||
'passive' means use compression when clients hint that data is
|
||||
compressible.
|
||||
'aggressive' means use compression unless clients hint that
|
||||
data is not compressible.
|
||||
'force' means use compression under all circumstances even if the clients
|
||||
hint that the data is not compressible.
|
||||
bluestore-compression-required-ratio:
|
||||
type: float
|
||||
default:
|
||||
description: |
|
||||
The ratio of the size of the data chunk after compression relative to the
|
||||
original size must be at least this small in order to store the
|
||||
compressed version on pools requested by this charm.
|
||||
bluestore-compression-min-blob-size:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
Chunks smaller than this are never compressed on pools requested by
|
||||
this charm.
|
||||
bluestore-compression-min-blob-size-hdd:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
Value of bluestore compression min blob size for rotational media on
|
||||
pools requested by this charm.
|
||||
bluestore-compression-min-blob-size-ssd:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
Value of bluestore compression min blob size for solid state media on
|
||||
pools requested by this charm.
|
||||
bluestore-compression-max-blob-size:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
Chunks larger than this are broken into smaller blobs sizing bluestore
|
||||
compression max blob size before being compressed on pools requested by
|
||||
this charm.
|
||||
bluestore-compression-max-blob-size-hdd:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
Value of bluestore compression max blob size for rotational media on
|
||||
pools requested by this charm.
|
||||
bluestore-compression-max-blob-size-ssd:
|
||||
type: int
|
||||
default:
|
||||
description: |
|
||||
Value of bluestore compression max blob size for solid state media on
|
||||
pools requested by this charm.
|
@@ -0,0 +1,211 @@
|
||||
"""Library for the ingress relation.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the ingress interface.
|
||||
|
||||
Import `IngressRequires` in your charm, with two required options:
|
||||
- "self" (the charm itself)
|
||||
- config_dict
|
||||
|
||||
`config_dict` accepts the following keys:
|
||||
- service-hostname (required)
|
||||
- service-name (required)
|
||||
- service-port (required)
|
||||
- additional-hostnames
|
||||
- limit-rps
|
||||
- limit-whitelist
|
||||
- max-body-size
|
||||
- path-routes
|
||||
- retry-errors
|
||||
- rewrite-enabled
|
||||
- rewrite-target
|
||||
- service-namespace
|
||||
- session-cookie-max-age
|
||||
- tls-secret-name
|
||||
|
||||
See [the config section](https://charmhub.io/nginx-ingress-integrator/configure) for descriptions
|
||||
of each, along with the required type.
|
||||
|
||||
As an example, add the following to `src/charm.py`:
|
||||
```
|
||||
from charms.nginx_ingress_integrator.v0.ingress import IngressRequires
|
||||
|
||||
# In your charm's `__init__` method.
|
||||
self.ingress = IngressRequires(self, {"service-hostname": self.config["external_hostname"],
|
||||
"service-name": self.app.name,
|
||||
"service-port": 80})
|
||||
|
||||
# In your charm's `config-changed` handler.
|
||||
self.ingress.update_config({"service-hostname": self.config["external_hostname"]})
|
||||
```
|
||||
And then add the following to `metadata.yaml`:
|
||||
```
|
||||
requires:
|
||||
ingress:
|
||||
interface: ingress
|
||||
```
|
||||
You _must_ register the IngressRequires class as part of the `__init__` method
|
||||
rather than, for instance, a config-changed event handler. This is because
|
||||
doing so won't get the current relation changed event, because it wasn't
|
||||
registered to handle the event (because it wasn't created in `__init__` when
|
||||
the event was fired).
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ops.charm import CharmEvents
|
||||
from ops.framework import EventBase, EventSource, Object
|
||||
from ops.model import BlockedStatus
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "db0af4367506491c91663468fb5caa4c"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 9
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REQUIRED_INGRESS_RELATION_FIELDS = {
|
||||
"service-hostname",
|
||||
"service-name",
|
||||
"service-port",
|
||||
}
|
||||
|
||||
OPTIONAL_INGRESS_RELATION_FIELDS = {
|
||||
"additional-hostnames",
|
||||
"limit-rps",
|
||||
"limit-whitelist",
|
||||
"max-body-size",
|
||||
"retry-errors",
|
||||
"rewrite-target",
|
||||
"rewrite-enabled",
|
||||
"service-namespace",
|
||||
"session-cookie-max-age",
|
||||
"tls-secret-name",
|
||||
"path-routes",
|
||||
}
|
||||
|
||||
|
||||
class IngressAvailableEvent(EventBase):
|
||||
pass
|
||||
|
||||
|
||||
class IngressCharmEvents(CharmEvents):
|
||||
"""Custom charm events."""
|
||||
|
||||
ingress_available = EventSource(IngressAvailableEvent)
|
||||
|
||||
|
||||
class IngressRequires(Object):
|
||||
"""This class defines the functionality for the 'requires' side of the 'ingress' relation.
|
||||
|
||||
Hook events observed:
|
||||
- relation-changed
|
||||
"""
|
||||
|
||||
def __init__(self, charm, config_dict):
|
||||
super().__init__(charm, "ingress")
|
||||
|
||||
self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
|
||||
|
||||
self.config_dict = config_dict
|
||||
|
||||
def _config_dict_errors(self, update_only=False):
|
||||
"""Check our config dict for errors."""
|
||||
blocked_message = "Error in ingress relation, check `juju debug-log`"
|
||||
unknown = [
|
||||
x
|
||||
for x in self.config_dict
|
||||
if x not in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
|
||||
]
|
||||
if unknown:
|
||||
logger.error(
|
||||
"Ingress relation error, unknown key(s) in config dictionary found: %s",
|
||||
", ".join(unknown),
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(blocked_message)
|
||||
return True
|
||||
if not update_only:
|
||||
missing = [x for x in REQUIRED_INGRESS_RELATION_FIELDS if x not in self.config_dict]
|
||||
if missing:
|
||||
logger.error(
|
||||
"Ingress relation error, missing required key(s) in config dictionary: %s",
|
||||
", ".join(missing),
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(blocked_message)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _on_relation_changed(self, event):
|
||||
"""Handle the relation-changed event."""
|
||||
# `self.unit` isn't available here, so use `self.model.unit`.
|
||||
if self.model.unit.is_leader():
|
||||
if self._config_dict_errors():
|
||||
return
|
||||
for key in self.config_dict:
|
||||
event.relation.data[self.model.app][key] = str(self.config_dict[key])
|
||||
|
||||
def update_config(self, config_dict):
|
||||
"""Allow for updates to relation."""
|
||||
if self.model.unit.is_leader():
|
||||
self.config_dict = config_dict
|
||||
if self._config_dict_errors(update_only=True):
|
||||
return
|
||||
relation = self.model.get_relation("ingress")
|
||||
if relation:
|
||||
for key in self.config_dict:
|
||||
relation.data[self.model.app][key] = str(self.config_dict[key])
|
||||
|
||||
|
||||
class IngressProvides(Object):
|
||||
"""This class defines the functionality for the 'provides' side of the 'ingress' relation.
|
||||
|
||||
Hook events observed:
|
||||
- relation-changed
|
||||
"""
|
||||
|
||||
def __init__(self, charm):
|
||||
super().__init__(charm, "ingress")
|
||||
# Observe the relation-changed hook event and bind
|
||||
# self.on_relation_changed() to handle the event.
|
||||
self.framework.observe(charm.on["ingress"].relation_changed, self._on_relation_changed)
|
||||
self.charm = charm
|
||||
|
||||
def _on_relation_changed(self, event):
|
||||
"""Handle a change to the ingress relation.
|
||||
|
||||
Confirm we have the fields we expect to receive."""
|
||||
# `self.unit` isn't available here, so use `self.model.unit`.
|
||||
if not self.model.unit.is_leader():
|
||||
return
|
||||
|
||||
ingress_data = {
|
||||
field: event.relation.data[event.app].get(field)
|
||||
for field in REQUIRED_INGRESS_RELATION_FIELDS | OPTIONAL_INGRESS_RELATION_FIELDS
|
||||
}
|
||||
|
||||
missing_fields = sorted(
|
||||
[
|
||||
field
|
||||
for field in REQUIRED_INGRESS_RELATION_FIELDS
|
||||
if ingress_data.get(field) is None
|
||||
]
|
||||
)
|
||||
|
||||
if missing_fields:
|
||||
logger.error(
|
||||
"Missing required data fields for ingress relation: {}".format(
|
||||
", ".join(missing_fields)
|
||||
)
|
||||
)
|
||||
self.model.unit.status = BlockedStatus(
|
||||
"Missing fields for ingress: {}".format(", ".join(missing_fields))
|
||||
)
|
||||
|
||||
# Create an event that our charm can use to decide it's okay to
|
||||
# configure the ingress.
|
||||
self.charm.on.ingress_available.emit()
|
@@ -0,0 +1,193 @@
|
||||
# Copyright 2021 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
"""# KubernetesServicePatch Library.
|
||||
|
||||
This library is designed to enable developers to more simply patch the Kubernetes Service created
|
||||
by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
|
||||
service named after the application in the namespace (named after the Juju model). This service by
|
||||
default contains a "placeholder" port, which is 65536/TCP.
|
||||
|
||||
When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
|
||||
charm. In this case, any modifications to the default service (created during deployment), will
|
||||
be overwritten during a charm upgrade.
|
||||
|
||||
When intialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
|
||||
events which applies the patch to the cluster. This should ensure that the service ports are
|
||||
correct throughout the charm's life.
|
||||
|
||||
The constructor simply takes a reference to the parent charm, and a list of tuples that each define
|
||||
a port for the service, where each tuple contains:
|
||||
|
||||
- a name for the port
|
||||
- port for the service to listen on
|
||||
- optionally: a targetPort for the service (the port in the container!)
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started using the library, you just need to fetch the library using `charmcraft`. **Note
|
||||
that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
|
||||
|
||||
```shell
|
||||
cd some-charm
|
||||
charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
|
||||
echo <<-EOF >> requirements.txt
|
||||
lightkube
|
||||
lightkube-models
|
||||
EOF
|
||||
```
|
||||
|
||||
Then, to initialise the library:
|
||||
|
||||
```python
|
||||
# ...
|
||||
from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
|
||||
|
||||
class SomeCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
# ...
|
||||
self.service_patcher = KubernetesServicePatch(self, [(f"{self.app.name}", 8080)])
|
||||
# ...
|
||||
```
|
||||
|
||||
Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
|
||||
does not try to make any API calls, or open any files during testing that are unlikely to be
|
||||
present, and could break your tests. The easiest way to do this is during your test `setUp`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
@patch("charm.KubernetesServicePatch", lambda x, y: None)
|
||||
def setUp(self, *unused):
|
||||
self.harness = Harness(SomeCharm)
|
||||
# ...
|
||||
```
|
||||
"""
|
||||
|
||||
import logging
|
||||
from types import MethodType
|
||||
from typing import Sequence, Tuple, Union
|
||||
|
||||
from lightkube import ApiError, Client
|
||||
from lightkube.models.core_v1 import ServicePort, ServiceSpec
|
||||
from lightkube.models.meta_v1 import ObjectMeta
|
||||
from lightkube.resources.core_v1 import Service
|
||||
from lightkube.types import PatchType
|
||||
from ops.charm import CharmBase
|
||||
from ops.framework import Object
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "0042f86d0a874435adef581806cddbbb"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 3
|
||||
|
||||
PortDefinition = Union[Tuple[str, int], Tuple[str, int, int]]
|
||||
|
||||
|
||||
class KubernetesServicePatch(Object):
|
||||
"""A utility for patching the Kubernetes service set up by Juju."""
|
||||
|
||||
def __init__(self, charm: CharmBase, ports: Sequence[PortDefinition]):
|
||||
"""Constructor for KubernetesServicePatch.
|
||||
|
||||
Args:
|
||||
charm: the charm that is instantiating the library.
|
||||
ports: a list of tuples (name, port, targetPort) for every service port.
|
||||
"""
|
||||
super().__init__(charm, "kubernetes-service-patch")
|
||||
self.charm = charm
|
||||
self.service = self._service_object(ports)
|
||||
|
||||
# Make mypy type checking happy that self._patch is a method
|
||||
assert isinstance(self._patch, MethodType)
|
||||
# Ensure this patch is applied during the 'install' and 'upgrade-charm' events
|
||||
self.framework.observe(charm.on.install, self._patch)
|
||||
self.framework.observe(charm.on.upgrade_charm, self._patch)
|
||||
|
||||
def _service_object(self, ports: Sequence[PortDefinition]) -> Service:
|
||||
"""Creates a valid Service representation for Alertmanager.
|
||||
|
||||
Args:
|
||||
ports: a list of tuples of the form (name, port) or (name, port, targetPort) for every
|
||||
service port. If the 'targetPort' is omitted, it is assumed to be equal to 'port'.
|
||||
|
||||
Returns:
|
||||
Service: A valid representation of a Kubernetes Service with the correct ports.
|
||||
"""
|
||||
return Service(
|
||||
apiVersion="v1",
|
||||
kind="Service",
|
||||
metadata=ObjectMeta(
|
||||
namespace=self._namespace,
|
||||
name=self._app,
|
||||
labels={"app.kubernetes.io/name": self._app},
|
||||
),
|
||||
spec=ServiceSpec(
|
||||
selector={"app.kubernetes.io/name": self._app},
|
||||
ports=[
|
||||
ServicePort(name=p[0], port=p[1], targetPort=p[2] if len(p) > 2 else p[1]) # type: ignore
|
||||
for p in ports
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def _patch(self, _) -> None:
|
||||
"""Patch the Kubernetes service created by Juju to map the correct port.
|
||||
|
||||
Raises:
|
||||
PatchFailed: if patching fails due to lack of permissions, or otherwise.
|
||||
"""
|
||||
if not self.charm.unit.is_leader():
|
||||
return
|
||||
|
||||
client = Client()
|
||||
try:
|
||||
client.patch(Service, self._app, self.service, patch_type=PatchType.MERGE)
|
||||
except ApiError as e:
|
||||
if e.status.code == 403:
|
||||
logger.error("Kubernetes service patch failed: `juju trust` this application.")
|
||||
else:
|
||||
logger.error("Kubernetes service patch failed: %s", str(e))
|
||||
else:
|
||||
logger.info("Kubernetes service '%s' patched successfully", self._app)
|
||||
|
||||
def is_patched(self) -> bool:
|
||||
"""Reports if the service patch has been applied.
|
||||
|
||||
Returns:
|
||||
bool: A boolean indicating if the service patch has been applied.
|
||||
"""
|
||||
client = Client()
|
||||
# Get the relevant service from the cluster
|
||||
service = client.get(Service, name=self._app, namespace=self._namespace)
|
||||
# Construct a list of expected ports, should the patch be applied
|
||||
expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
|
||||
# Construct a list in the same manner, using the fetched service
|
||||
fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports]
|
||||
return expected_ports == fetched_ports
|
||||
|
||||
@property
|
||||
def _app(self) -> str:
|
||||
"""Name of the current Juju application.
|
||||
|
||||
Returns:
|
||||
str: A string containing the name of the current Juju application.
|
||||
"""
|
||||
return self.charm.app.name
|
||||
|
||||
@property
|
||||
def _namespace(self) -> str:
|
||||
"""The Kubernetes namespace we're running in.
|
||||
|
||||
Returns:
|
||||
str: A string containing the name of the current Kubernetes namespace.
|
||||
"""
|
||||
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f:
|
||||
return f.read().strip()
|
@@ -0,0 +1,470 @@
|
||||
"""IdentityServiceProvides and Requires module.
|
||||
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the identity_service interface.
|
||||
|
||||
Import `IdentityServiceRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "identity_service"
|
||||
|
||||
Also provide additional parameters to the charm object:
|
||||
- service
|
||||
- internal_url
|
||||
- public_url
|
||||
- admin_url
|
||||
- region
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.sunbeam_sunbeam_identity_service_operator.v0.identity_service import IdentityServiceRequires
|
||||
|
||||
class IdentityServiceClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# IdentityService Requires
|
||||
self.identity_service = IdentityServiceRequires(
|
||||
self, "identity_service",
|
||||
service = "my-service"
|
||||
internal_url = "http://internal-url"
|
||||
public_url = "http://public-url"
|
||||
admin_url = "http://admin-url"
|
||||
region = "region"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.connected, self._on_identity_service_connected)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.ready, self._on_identity_service_ready)
|
||||
self.framework.observe(
|
||||
self.identity_service.on.goneaway, self._on_identity_service_goneaway)
|
||||
|
||||
def _on_identity_service_connected(self, event):
|
||||
'''React to the IdentityService connected event.
|
||||
|
||||
This event happens when n IdentityService relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_identity_service_ready(self, event):
|
||||
'''React to the IdentityService ready event.
|
||||
|
||||
The IdentityService interface will use the provided config for the
|
||||
request to the identity server.
|
||||
'''
|
||||
# IdentityService Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_identity_service_goneaway(self, event):
|
||||
'''React to the IdentityService goneaway event.
|
||||
|
||||
This event happens when an IdentityService relation is removed.
|
||||
'''
|
||||
# IdentityService Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "6a7cb19b98314ecf916e3fcb02708608"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 1
|
||||
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
from ops.model import Relation
|
||||
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IdentityServiceConnectedEvent(EventBase):
|
||||
"""IdentityService connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceReadyEvent(EventBase):
|
||||
"""IdentityService ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceGoneAwayEvent(EventBase):
|
||||
"""IdentityService relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IdentityServiceServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(IdentityServiceConnectedEvent)
|
||||
ready = EventSource(IdentityServiceReadyEvent)
|
||||
goneaway = EventSource(IdentityServiceGoneAwayEvent)
|
||||
|
||||
|
||||
class IdentityServiceRequires(Object):
|
||||
"""
|
||||
IdentityServiceRequires class
|
||||
"""
|
||||
|
||||
on = IdentityServiceServerEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name: str, service_endpoints: dict,
|
||||
region: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""IdentityService relation joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.connected.emit()
|
||||
self.register_services(
|
||||
self.service_endpoints,
|
||||
self.region)
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""IdentityService relation changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
try:
|
||||
self.service_password
|
||||
self.on.ready.emit()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""IdentityService relation broken."""
|
||||
logging.debug("IdentityService on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _identity_service_rel(self) -> Relation:
|
||||
"""The IdentityService relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
def get_remote_app_data(self, key: str) -> str:
|
||||
"""Return the value for the given key from remote app data."""
|
||||
data = self._identity_service_rel.data[self._identity_service_rel.app]
|
||||
return data.get(key)
|
||||
|
||||
@property
|
||||
def api_version(self) -> str:
|
||||
"""Return the api_version."""
|
||||
return self.get_remote_app_data('api-version')
|
||||
|
||||
@property
|
||||
def auth_host(self) -> str:
|
||||
"""Return the auth_host."""
|
||||
return self.get_remote_app_data('auth-host')
|
||||
|
||||
@property
|
||||
def auth_port(self) -> str:
|
||||
"""Return the auth_port."""
|
||||
return self.get_remote_app_data('auth-port')
|
||||
|
||||
@property
|
||||
def auth_protocol(self) -> str:
|
||||
"""Return the auth_protocol."""
|
||||
return self.get_remote_app_data('auth-protocol')
|
||||
|
||||
@property
|
||||
def internal_host(self) -> str:
|
||||
"""Return the internal_host."""
|
||||
return self.get_remote_app_data('internal-host')
|
||||
|
||||
@property
|
||||
def internal_port(self) -> str:
|
||||
"""Return the internal_port."""
|
||||
return self.get_remote_app_data('internal-port')
|
||||
|
||||
@property
|
||||
def internal_protocol(self) -> str:
|
||||
"""Return the internal_protocol."""
|
||||
return self.get_remote_app_data('internal-protocol')
|
||||
|
||||
@property
|
||||
def admin_domain_name(self) -> str:
|
||||
"""Return the admin_domain_name."""
|
||||
return self.get_remote_app_data('admin-domain-name')
|
||||
|
||||
@property
|
||||
def admin_domain_id(self) -> str:
|
||||
"""Return the admin_domain_id."""
|
||||
return self.get_remote_app_data('admin-domain-id')
|
||||
|
||||
@property
|
||||
def admin_project_name(self) -> str:
|
||||
"""Return the admin_project_name."""
|
||||
return self.get_remote_app_data('admin-project-name')
|
||||
|
||||
@property
|
||||
def admin_project_id(self) -> str:
|
||||
"""Return the admin_project_id."""
|
||||
return self.get_remote_app_data('admin-project-id')
|
||||
|
||||
@property
|
||||
def admin_user_name(self) -> str:
|
||||
"""Return the admin_user_name."""
|
||||
return self.get_remote_app_data('admin-user-name')
|
||||
|
||||
@property
|
||||
def admin_user_id(self) -> str:
|
||||
"""Return the admin_user_id."""
|
||||
return self.get_remote_app_data('admin-user-id')
|
||||
|
||||
@property
|
||||
def service_domain_name(self) -> str:
|
||||
"""Return the service_domain_name."""
|
||||
return self.get_remote_app_data('service-domain-name')
|
||||
|
||||
@property
|
||||
def service_domain_id(self) -> str:
|
||||
"""Return the service_domain_id."""
|
||||
return self.get_remote_app_data('service-domain-id')
|
||||
|
||||
@property
|
||||
def service_host(self) -> str:
|
||||
"""Return the service_host."""
|
||||
return self.get_remote_app_data('service-host')
|
||||
|
||||
@property
|
||||
def service_password(self) -> str:
|
||||
"""Return the service_password."""
|
||||
return self.get_remote_app_data('service-password')
|
||||
|
||||
@property
|
||||
def service_port(self) -> str:
|
||||
"""Return the service_port."""
|
||||
return self.get_remote_app_data('service-port')
|
||||
|
||||
@property
|
||||
def service_protocol(self) -> str:
|
||||
"""Return the service_protocol."""
|
||||
return self.get_remote_app_data('service-protocol')
|
||||
|
||||
@property
|
||||
def service_project_name(self) -> str:
|
||||
"""Return the service_project_name."""
|
||||
return self.get_remote_app_data('service-project-name')
|
||||
|
||||
@property
|
||||
def service_project_id(self) -> str:
|
||||
"""Return the service_project_id."""
|
||||
return self.get_remote_app_data('service-project-id')
|
||||
|
||||
@property
|
||||
def service_user_name(self) -> str:
|
||||
"""Return the service_user_name."""
|
||||
return self.get_remote_app_data('service-user-name')
|
||||
|
||||
@property
|
||||
def service_user_id(self) -> str:
|
||||
"""Return the service_user_id."""
|
||||
return self.get_remote_app_data('service-user-id')
|
||||
|
||||
|
||||
def register_services(self, service_endpoints: dict,
|
||||
region: str) -> None:
|
||||
"""Request access to the IdentityService server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting service registration")
|
||||
app_data = self._identity_service_rel.data[self.charm.app]
|
||||
app_data["service-endpoints"] = json.dumps(service_endpoints)
|
||||
app_data["region"] = region
|
||||
|
||||
|
||||
class HasIdentityServiceClientsEvent(EventBase):
|
||||
"""Has IdentityServiceClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyIdentityServiceClientsEvent(EventBase):
|
||||
"""IdentityServiceClients Ready Event."""
|
||||
|
||||
def __init__(self, handle, relation_id, relation_name, service_endpoints,
|
||||
region, client_app_name):
|
||||
super().__init__(handle)
|
||||
self.relation_id = relation_id
|
||||
self.relation_name = relation_name
|
||||
self.service_endpoints = service_endpoints
|
||||
self.region = region
|
||||
self.client_app_name = client_app_name
|
||||
|
||||
|
||||
def snapshot(self):
|
||||
return {
|
||||
"relation_id": self.relation_id,
|
||||
"relation_name": self.relation_name,
|
||||
"service_endpoints": self.service_endpoints,
|
||||
"client_app_name": self.client_app_name,
|
||||
"region": self.region}
|
||||
|
||||
def restore(self, snapshot):
|
||||
super().restore(snapshot)
|
||||
self.relation_id = snapshot["relation_id"]
|
||||
self.relation_name = snapshot["relation_name"]
|
||||
self.service_endpoints = snapshot["service_endpoints"]
|
||||
self.region = snapshot["region"]
|
||||
self.client_app_name = snapshot["client_app_name"]
|
||||
|
||||
|
||||
class IdentityServiceClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent)
|
||||
ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent)
|
||||
|
||||
|
||||
class IdentityServiceProvides(Object):
|
||||
"""
|
||||
IdentityServiceProvides class
|
||||
"""
|
||||
|
||||
on = IdentityServiceClientEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_identity_service_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_identity_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_identity_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_identity_service_relation_joined(self, event):
|
||||
"""Handle IdentityService joined."""
|
||||
logging.debug("IdentityService on_joined")
|
||||
self.on.has_identity_service_clients.emit()
|
||||
|
||||
def _on_identity_service_relation_changed(self, event):
|
||||
"""Handle IdentityService changed."""
|
||||
logging.debug("IdentityService on_changed")
|
||||
REQUIRED_KEYS = [
|
||||
'service-endpoints',
|
||||
'region']
|
||||
|
||||
values = [
|
||||
event.relation.data[event.relation.app].get(k)
|
||||
for k in REQUIRED_KEYS ]
|
||||
# Validate data on the relation
|
||||
if all(values):
|
||||
print(event.relation.id)
|
||||
print(event.relation.name)
|
||||
service_eps = json.loads(
|
||||
event.relation.data[event.relation.app]['service-endpoints'])
|
||||
self.on.ready_identity_service_clients.emit(
|
||||
event.relation.id,
|
||||
event.relation.name,
|
||||
service_eps,
|
||||
event.relation.data[event.relation.app]['region'],
|
||||
event.relation.app.name)
|
||||
|
||||
def _on_identity_service_relation_broken(self, event):
|
||||
"""Handle IdentityService broken."""
|
||||
logging.debug("IdentityServiceProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def set_identity_service_credentials(self, relation_name: int,
|
||||
relation_id: str,
|
||||
api_version: str,
|
||||
auth_host: str,
|
||||
auth_port: str,
|
||||
auth_protocol: str,
|
||||
internal_host: str,
|
||||
internal_port: str,
|
||||
internal_protocol: str,
|
||||
service_host: str,
|
||||
service_port: str,
|
||||
service_protocol: str,
|
||||
admin_domain: str,
|
||||
admin_project: str,
|
||||
admin_user: str,
|
||||
service_domain: str,
|
||||
service_password: str,
|
||||
service_project: str,
|
||||
service_user: str):
|
||||
logging.debug("Setting identity_service connection information.")
|
||||
for relation in self.framework.model.relations[relation_name]:
|
||||
if relation.id == relation_id:
|
||||
_identity_service_rel = relation
|
||||
app_data = _identity_service_rel.data[self.charm.app]
|
||||
app_data["api-version"] = api_version
|
||||
app_data["auth-host"] = auth_host
|
||||
app_data["auth-port"] = str(auth_port)
|
||||
app_data["auth-protocol"] = auth_protocol
|
||||
app_data["internal-host"] = internal_host
|
||||
app_data["internal-port"] = str(internal_port)
|
||||
app_data["internal-protocol"] = internal_protocol
|
||||
app_data["service-host"] = service_host
|
||||
app_data["service-port"] = str(service_port)
|
||||
app_data["service-protocol"] = service_protocol
|
||||
app_data["admin-domain-name"] = admin_domain.name
|
||||
app_data["admin-domain-id"] = admin_domain.id
|
||||
app_data["admin-project-name"] = admin_project.name
|
||||
app_data["admin-project-id"] = admin_project.id
|
||||
app_data["admin-user-name"] = admin_user.name
|
||||
app_data["admin-user-id"] = admin_user.id
|
||||
app_data["service-domain-name"] = service_domain.name
|
||||
app_data["service-domain-id"] = service_domain.id
|
||||
app_data["service-project-name"] = service_project.name
|
||||
app_data["service-project-id"] = service_project.id
|
||||
app_data["service-user-name"] = service_user.name
|
||||
app_data["service-user-id"] = service_user.id
|
||||
app_data["service-password"] = service_password
|
165
charms/glance-k8s/lib/charms/sunbeam_mysql_k8s/v0/mysql.py
Normal file
165
charms/glance-k8s/lib/charms/sunbeam_mysql_k8s/v0/mysql.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
## Overview
|
||||
|
||||
This document explains how to integrate with the MySQL charm for the purposes of consuming a mysql database. It also explains how alternative implementations of the MySQL charm may maintain the same interface and be backward compatible with all currently integrated charms. Finally this document is the authoritative reference on the structure of relation data that is shared between MySQL charms and any other charm that intends to use the database.
|
||||
|
||||
|
||||
## Consumer Library Usage
|
||||
|
||||
The MySQL charm library uses the [Provider and Consumer](https://ops.readthedocs.io/en/latest/#module-ops.relation) objects from the Operator Framework. Charms that would like to use a MySQL database must use the `MySQLConsumer` object from the charm library. Using the `MySQLConsumer` object requires instantiating it, typically in the constructor of your charm. The `MySQLConsumer` constructor requires the name of the relation over which a database will be used. This relation must use the `mysql_datastore` interface. In addition the constructor also requires a `consumes` specification, which is a dictionary with key `mysql` (also see Provider Library Usage below) and a value that represents the minimum acceptable version of MySQL. This version string can be in any format that is compatible with the Python [Semantic Version module](https://pypi.org/project/semantic-version/). For example, assuming your charm consumes a database over a rlation named "monitoring", you may instantiate `MySQLConsumer` as follows:
|
||||
|
||||
from charms.mysql_k8s.v0.mysql import MySQLConsumer
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
...
|
||||
self.mysql_consumer = MySQLConsumer(
|
||||
self, "monitoring", {"mysql": ">=8"}
|
||||
)
|
||||
...
|
||||
|
||||
This example hard codes the consumes dictionary argument containing the minimal MySQL version required, however you may want to consider generating this dictionary by some other means, such as a `self.consumes` property in your charm. This is because the minimum required MySQL version may change when you upgrade your charm. Of course it is expected that you will keep this version string updated as you develop newer releases of your charm. If the version string can be determined at run time by inspecting the actual deployed version of your charmed application, this would be ideal.
|
||||
An instantiated `MySQLConsumer` object may be used to request new databases using the `new_database()` method. This method requires no arguments unless you require multiple databases. If multiple databases are requested, you must provide a unique `name_suffix` argument. For example:
|
||||
|
||||
def _on_database_relation_joined(self, event):
|
||||
self.mysql_consumer.new_database(name_suffix="db1")
|
||||
self.mysql_consumer.new_database(name_suffix="db2")
|
||||
|
||||
The `address`, `port`, `databases`, and `credentials` methods can all be called
|
||||
to get the relevant information from the relation data.
|
||||
"""
|
||||
|
||||
# !/usr/bin/env python3
|
||||
# Copyright 2021 Canonical Ltd.
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
from ops.relation import ConsumerBase
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "1fdc567d7095465990dc1f9be80461fd"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 1
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DatabaseConnectedEvent(EventBase):
|
||||
"""Database connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseReadyEvent(EventBase):
|
||||
"""Database ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseGoneAwayEvent(EventBase):
|
||||
"""Database relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(DatabaseConnectedEvent)
|
||||
ready = EventSource(DatabaseReadyEvent)
|
||||
goneaway = EventSource(DatabaseGoneAwayEvent)
|
||||
|
||||
|
||||
class MySQLConsumer(Object):
|
||||
"""
|
||||
MySQLConsumer lib class
|
||||
"""
|
||||
|
||||
on = DatabaseServerEvents()
|
||||
|
||||
def __init__(self, charm, relation_name: str, databases: list):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.request_databases = databases
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_database_relation_joined,
|
||||
)
|
||||
|
||||
def _on_database_relation_joined(self, event):
|
||||
"""AMQP relation joined."""
|
||||
logging.debug("DatabaseRequires on_joined")
|
||||
self.on.connected.emit()
|
||||
self.request_access(self.request_databases)
|
||||
|
||||
def databases(self, rel_id=None) -> list:
|
||||
"""
|
||||
List of currently available databases
|
||||
Returns:
|
||||
list: list of database names
|
||||
"""
|
||||
|
||||
rel = self.framework.model.get_relation(self.relation_name, rel_id)
|
||||
relation_data = rel.data[rel.app]
|
||||
dbs = relation_data.get("databases")
|
||||
databases = json.loads(dbs) if dbs else []
|
||||
|
||||
return databases
|
||||
|
||||
def credentials(self, rel_id=None) -> dict:
|
||||
"""
|
||||
Dictionary of credential information to access databases
|
||||
Returns:
|
||||
dict: dictionary of credential information including username,
|
||||
password and address
|
||||
"""
|
||||
rel = self.framework.model.get_relation(self.relation_name, rel_id)
|
||||
relation_data = rel.data[rel.app]
|
||||
data = relation_data.get("data")
|
||||
data = json.loads(data) if data else {}
|
||||
credentials = data.get("credentials")
|
||||
|
||||
return credentials
|
||||
|
||||
def new_database(self, rel_id=None, name_suffix=""):
|
||||
"""
|
||||
Request creation of an additional database
|
||||
"""
|
||||
if not self.charm.unit.is_leader():
|
||||
return
|
||||
|
||||
rel = self.framework.model.get_relation(self.relation_name, rel_id)
|
||||
|
||||
if name_suffix:
|
||||
name_suffix = "_{}".format(name_suffix)
|
||||
|
||||
rid = str(uuid.uuid4()).split("-")[-1]
|
||||
db_name = "db_{}_{}_{}".format(rel.id, rid, name_suffix)
|
||||
logger.debug("CLIENT REQUEST %s", db_name)
|
||||
rel_data = rel.data[self.charm.app]
|
||||
dbs = rel_data.get("databases")
|
||||
dbs = json.loads(dbs) if dbs else []
|
||||
dbs.append(db_name)
|
||||
rel.data[self.charm.app]["databases"] = json.dumps(dbs)
|
||||
|
||||
def request_access(self, databases: list) -> None:
|
||||
"""Request access to the AMQP server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting AMQP user and vhost")
|
||||
if databases:
|
||||
rel = self.framework.model.get_relation(self.relation_name)
|
||||
rel.data[self.charm.app]["databases"] = json.dumps(databases)
|
@@ -0,0 +1,314 @@
|
||||
"""AMQPProvides and Requires module.
|
||||
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the amqp interface.
|
||||
|
||||
Import `AMQPRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "amqp"
|
||||
|
||||
Also provide two additional parameters to the charm object:
|
||||
- username
|
||||
- vhost
|
||||
|
||||
Two events are also available to respond to:
|
||||
- connected
|
||||
- ready
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.sunbeam_rabbitmq_operator.v0.amqp import AMQPRequires
|
||||
|
||||
class AMQPClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# AMQP Requires
|
||||
self.amqp = AMQPRequires(
|
||||
self, "amqp",
|
||||
username="myusername",
|
||||
vhost="vhostname"
|
||||
)
|
||||
self.framework.observe(
|
||||
self.amqp.on.connected, self._on_amqp_connected)
|
||||
self.framework.observe(
|
||||
self.amqp.on.ready, self._on_amqp_ready)
|
||||
self.framework.observe(
|
||||
self.amqp.on.goneaway, self._on_amqp_goneaway)
|
||||
|
||||
def _on_amqp_connected(self, event):
|
||||
'''React to the AMQP connected event.
|
||||
|
||||
This event happens when n AMQP relation is added to the
|
||||
model before credentials etc have been provided.
|
||||
'''
|
||||
# Do something before the relation is complete
|
||||
pass
|
||||
|
||||
def _on_amqp_ready(self, event):
|
||||
'''React to the AMQP ready event.
|
||||
|
||||
The AMQP interface will use the provided username and vhost for the
|
||||
request to the rabbitmq server.
|
||||
'''
|
||||
# AMQP Relation is ready. Do something with the completed relation.
|
||||
pass
|
||||
|
||||
def _on_amqp_goneaway(self, event):
|
||||
'''React to the AMQP goneaway event.
|
||||
|
||||
This event happens when an AMQP relation is removed.
|
||||
'''
|
||||
# AMQP Relation has goneaway. shutdown services or suchlike
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "ab1414b6baf044f099caf9c117f1a101"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 3
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
EventBase,
|
||||
ObjectEvents,
|
||||
EventSource,
|
||||
Object,
|
||||
)
|
||||
|
||||
from ops.model import Relation
|
||||
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AMQPConnectedEvent(EventBase):
|
||||
"""AMQP connected Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AMQPReadyEvent(EventBase):
|
||||
"""AMQP ready for use Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AMQPGoneAwayEvent(EventBase):
|
||||
"""AMQP relation has gone-away Event"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AMQPServerEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
connected = EventSource(AMQPConnectedEvent)
|
||||
ready = EventSource(AMQPReadyEvent)
|
||||
goneaway = EventSource(AMQPGoneAwayEvent)
|
||||
|
||||
|
||||
class AMQPRequires(Object):
|
||||
"""
|
||||
AMQPRequires class
|
||||
"""
|
||||
|
||||
on = AMQPServerEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name: str, username: str, vhost: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.username = username
|
||||
self.vhost = vhost
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_departed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""AMQP relation joined."""
|
||||
logging.debug("RabbitMQAMQPRequires on_joined")
|
||||
self.on.connected.emit()
|
||||
self.request_access(self.username, self.vhost)
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""AMQP relation changed."""
|
||||
logging.debug("RabbitMQAMQPRequires on_changed")
|
||||
if self.password:
|
||||
self.on.ready.emit()
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""AMQP relation broken."""
|
||||
logging.debug("RabbitMQAMQPRequires on_broken")
|
||||
self.on.goneaway.emit()
|
||||
|
||||
@property
|
||||
def _amqp_rel(self) -> Relation:
|
||||
"""The AMQP relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
"""Return the AMQP password from the server side of the relation."""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("password")
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""Return the hostname from the AMQP relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("hostname")
|
||||
|
||||
@property
|
||||
def ssl_port(self) -> str:
|
||||
"""Return the SSL port from the AMQP relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port")
|
||||
|
||||
@property
|
||||
def ssl_ca(self) -> str:
|
||||
"""Return the SSL port from the AMQP relation"""
|
||||
return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca")
|
||||
|
||||
@property
|
||||
def hostnames(self) -> List[str]:
|
||||
"""Return a list of remote RMQ hosts from the AMQP relation"""
|
||||
_hosts = []
|
||||
for unit in self._amqp_rel.units:
|
||||
_hosts.append(self._amqp_rel.data[unit].get("ingress-address"))
|
||||
return _hosts
|
||||
|
||||
def request_access(self, username: str, vhost: str) -> None:
|
||||
"""Request access to the AMQP server."""
|
||||
if self.model.unit.is_leader():
|
||||
logging.debug("Requesting AMQP user and vhost")
|
||||
self._amqp_rel.data[self.charm.app]["username"] = username
|
||||
self._amqp_rel.data[self.charm.app]["vhost"] = vhost
|
||||
|
||||
|
||||
class HasAMQPClientsEvent(EventBase):
|
||||
"""Has AMQPClients Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReadyAMQPClientsEvent(EventBase):
|
||||
"""AMQPClients Ready Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AMQPClientEvents(ObjectEvents):
|
||||
"""Events class for `on`"""
|
||||
|
||||
has_amqp_clients = EventSource(HasAMQPClientsEvent)
|
||||
ready_amqp_clients = EventSource(ReadyAMQPClientsEvent)
|
||||
|
||||
|
||||
class AMQPProvides(Object):
|
||||
"""
|
||||
AMQPProvides class
|
||||
"""
|
||||
|
||||
on = AMQPClientEvents()
|
||||
_stored = StoredState()
|
||||
|
||||
def __init__(self, charm, relation_name):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_joined,
|
||||
self._on_amqp_relation_joined,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_amqp_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_amqp_relation_broken,
|
||||
)
|
||||
|
||||
def _on_amqp_relation_joined(self, event):
|
||||
"""Handle AMQP joined."""
|
||||
logging.debug("RabbitMQAMQPProvides on_joined")
|
||||
self.on.has_amqp_clients.emit()
|
||||
|
||||
def _on_amqp_relation_changed(self, event):
|
||||
"""Handle AMQP changed."""
|
||||
logging.debug("RabbitMQAMQPProvides on_changed")
|
||||
# Validate data on the relation
|
||||
if self.username(event) and self.vhost(event):
|
||||
self.on.ready_amqp_clients.emit()
|
||||
if self.charm.unit.is_leader():
|
||||
self.set_amqp_credentials(
|
||||
event, self.username(event), self.vhost(event)
|
||||
)
|
||||
|
||||
def _on_amqp_relation_broken(self, event):
|
||||
"""Handle AMQP broken."""
|
||||
logging.debug("RabbitMQAMQPProvides on_departed")
|
||||
# TODO clear data on the relation
|
||||
|
||||
def username(self, event):
|
||||
"""Return the AMQP username from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("username")
|
||||
|
||||
def vhost(self, event):
|
||||
"""Return the AMQP vhost from the client side of the relation."""
|
||||
return event.relation.data[event.relation.app].get("vhost")
|
||||
|
||||
def set_amqp_credentials(self, event, username, vhost):
|
||||
"""Set AMQP Credentials.
|
||||
|
||||
:param event: The current event
|
||||
:type EventsBase
|
||||
:param username: The requested username
|
||||
:type username: str
|
||||
:param vhost: The requested vhost
|
||||
:type vhost: str
|
||||
:returns: None
|
||||
:rtype: None
|
||||
"""
|
||||
# TODO: Can we move this into the charm code?
|
||||
# TODO TLS Support. Existing interfaces set ssl_port and ssl_ca
|
||||
logging.debug("Setting amqp connection information.")
|
||||
try:
|
||||
if not self.charm.does_vhost_exist(vhost):
|
||||
self.charm.create_vhost(vhost)
|
||||
password = self.charm.create_user(username)
|
||||
self.charm.set_user_permissions(username, vhost)
|
||||
event.relation.data[self.charm.app]["password"] = password
|
||||
event.relation.data[self.charm.app][
|
||||
"hostname"
|
||||
] = self.charm.hostname
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logging.warning(
|
||||
"Rabbitmq is not ready. Defering. Errno: {}".format(e.errno)
|
||||
)
|
||||
event.defer()
|
48
charms/glance-k8s/metadata.yaml
Normal file
48
charms/glance-k8s/metadata.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
# Copyright 2021 Canonical Ltd
|
||||
# See LICENSE file for licensing details.
|
||||
name: sunbeam-glance-operator
|
||||
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
|
||||
summary: OpenStack Image Registry and Delivery Service
|
||||
description: |
|
||||
The Glance project provides an image registration and discovery service
|
||||
and an image delivery service. These services are used in conjunction
|
||||
by Nova to deliver images from object stores, such as OpenStack's Swift
|
||||
service, to Nova's compute nodes.
|
||||
version: 3
|
||||
bases:
|
||||
- name: ubuntu
|
||||
channel: 20.04/stable
|
||||
tags:
|
||||
- openstack
|
||||
- storage
|
||||
- misc
|
||||
|
||||
containers:
|
||||
glance-api:
|
||||
resource: glance-api-image
|
||||
|
||||
resources:
|
||||
glance-api-image:
|
||||
type: oci-image
|
||||
description: OCI image for OpenStack Glance (kolla/glance-api-image)
|
||||
|
||||
requires:
|
||||
shared-db:
|
||||
interface: mysql_datastore
|
||||
limit: 1
|
||||
ingress:
|
||||
interface: ingress
|
||||
identity-service:
|
||||
interface: keystone
|
||||
limit: 1
|
||||
amqp:
|
||||
interface: rabbitmq
|
||||
image-service:
|
||||
interface: glance
|
||||
ceph:
|
||||
interface: ceph-client
|
||||
|
||||
peers:
|
||||
peers:
|
||||
interface: glance-peer
|
||||
|
8
charms/glance-k8s/requirements.txt
Normal file
8
charms/glance-k8s/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
# ops >= 1.2.0
|
||||
jinja2
|
||||
git+https://github.com/canonical/operator@2875e73e#egg=ops
|
||||
git+https://opendev.org/openstack/charm-ops-openstack#egg=ops_openstack
|
||||
git+https://github.com/openstack-charmers/advanced-sunbeam-openstack#egg=advanced_sunbeam_openstack
|
||||
lightkube
|
||||
git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client
|
||||
git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
|
17
charms/glance-k8s/run_tests
Executable file
17
charms/glance-k8s/run_tests
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh -e
|
||||
# Copyright 2021 liam
|
||||
# See LICENSE file for licensing details.
|
||||
|
||||
if [ -z "$VIRTUAL_ENV" -a -d venv/ ]; then
|
||||
. venv/bin/activate
|
||||
fi
|
||||
|
||||
if [ -z "$PYTHONPATH" ]; then
|
||||
export PYTHONPATH="lib:src"
|
||||
else
|
||||
export PYTHONPATH="lib:src:$PYTHONPATH"
|
||||
fi
|
||||
|
||||
flake8
|
||||
coverage run --branch --source=src -m unittest -v "$@"
|
||||
coverage report -m
|
182
charms/glance-k8s/src/charm.py
Executable file
182
charms/glance-k8s/src/charm.py
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Glance Operator Charm.
|
||||
|
||||
This charm provide Glance services as part of an OpenStack deployment
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from ops.framework import StoredState
|
||||
from ops.main import main
|
||||
|
||||
import advanced_sunbeam_openstack.cprocess as sunbeam_cprocess
|
||||
import advanced_sunbeam_openstack.charm as sunbeam_charm
|
||||
import advanced_sunbeam_openstack.core as sunbeam_core
|
||||
import advanced_sunbeam_openstack.relation_handlers as sunbeam_rhandlers
|
||||
import advanced_sunbeam_openstack.config_contexts as sunbeam_ctxts
|
||||
|
||||
from charms.observability_libs.v0.kubernetes_service_patch \
|
||||
import KubernetesServicePatch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
"""Charm the service."""
|
||||
|
||||
ceph_conf = "/etc/ceph/ceph.conf"
|
||||
|
||||
_state = StoredState()
|
||||
_authed = False
|
||||
service_name = "glance-api"
|
||||
wsgi_admin_script = '/usr/bin/glance-wsgi-api'
|
||||
wsgi_public_script = '/usr/bin/glance-wsgi-api'
|
||||
|
||||
def __init__(self, framework):
|
||||
super().__init__(framework)
|
||||
self.service_patcher = KubernetesServicePatch(
|
||||
self,
|
||||
[
|
||||
('public', self.default_public_ingress_port),
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]:
|
||||
"""Configuration contexts for the operator."""
|
||||
contexts = super().config_contexts
|
||||
contexts.append(
|
||||
sunbeam_ctxts.CephConfigurationContext(self, "ceph_config"))
|
||||
contexts.append(
|
||||
sunbeam_ctxts.CinderCephConfigurationContext(self, "cinder_ceph"))
|
||||
return contexts
|
||||
|
||||
@property
|
||||
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
|
||||
"""Container configurations for the operator."""
|
||||
_cconfigs = super().container_configs
|
||||
_cconfigs.extend(
|
||||
[
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
[self.service_name],
|
||||
self.ceph_conf,
|
||||
self.service_user,
|
||||
self.service_group,
|
||||
),
|
||||
]
|
||||
)
|
||||
return _cconfigs
|
||||
|
||||
def get_relation_handlers(self) -> List[sunbeam_rhandlers.RelationHandler]:
|
||||
"""Relation handlers for the service."""
|
||||
handlers = super().get_relation_handlers()
|
||||
self.ceph = sunbeam_rhandlers.CephClientHandler(
|
||||
self,
|
||||
"ceph",
|
||||
self.configure_charm,
|
||||
allow_ec_overwrites=True,
|
||||
app_name='rbd'
|
||||
)
|
||||
handlers.append(self.ceph)
|
||||
return handlers
|
||||
|
||||
@property
|
||||
def service_conf(self) -> str:
|
||||
"""Service default configuration file."""
|
||||
return f"/etc/glance/glance-api.conf"
|
||||
|
||||
@property
|
||||
def service_user(self) -> str:
|
||||
"""Service user file and directory ownership."""
|
||||
return 'glance'
|
||||
|
||||
@property
|
||||
def service_group(self) -> str:
|
||||
"""Service group file and directory ownership."""
|
||||
return 'glance'
|
||||
|
||||
@property
|
||||
def service_endpoints(self):
|
||||
return [
|
||||
{
|
||||
'service_name': 'glance',
|
||||
'type': 'image',
|
||||
'description': "OpenStack Image",
|
||||
'internal_url': f'{self.internal_url}',
|
||||
'public_url': f'{self.public_url}',
|
||||
'admin_url': f'{self.admin_url}'}]
|
||||
|
||||
@property
|
||||
def default_public_ingress_port(self):
|
||||
return 9292
|
||||
|
||||
def _do_bootstrap(self):
|
||||
"""
|
||||
Starts the appropriate services in the order they are needed.
|
||||
If the service has not yet been bootstrapped, then this will
|
||||
1. Create the database
|
||||
"""
|
||||
super()._do_bootstrap()
|
||||
try:
|
||||
container = self.unit.get_container(self.wsgi_container_name)
|
||||
logger.info("Syncing database...")
|
||||
out = sunbeam_cprocess.check_output(
|
||||
container,
|
||||
[
|
||||
'sudo', '-u', 'glance',
|
||||
'glance-manage', '--config-dir',
|
||||
'/etc/glance', 'db', 'sync'],
|
||||
service_name='keystone-db-sync',
|
||||
timeout=180)
|
||||
logging.debug(f'Output from database sync: \n{out}')
|
||||
except sunbeam_cprocess.ContainerProcessError:
|
||||
logger.exception('Failed to bootstrap')
|
||||
self._state.bootstrapped = False
|
||||
return
|
||||
|
||||
def configure_charm(self, event) -> None:
|
||||
"""Catchall handler to cconfigure charm services."""
|
||||
if not self.relation_handlers_ready():
|
||||
logging.debug("Defering configuration, charm relations not ready")
|
||||
return
|
||||
|
||||
for ph in self.pebble_handlers:
|
||||
if ph.pebble_ready:
|
||||
container = self.unit.get_container(
|
||||
ph.container_name
|
||||
)
|
||||
sunbeam_cprocess.check_call(
|
||||
container,
|
||||
['apt', 'update'])
|
||||
sunbeam_cprocess.check_call(
|
||||
container,
|
||||
['apt', 'install', '-y', 'ceph-common'])
|
||||
try:
|
||||
sunbeam_cprocess.check_call(
|
||||
container,
|
||||
['ceph-authtool',
|
||||
f'/etc/ceph/ceph.client.{self.app.name}.keyring',
|
||||
'--create-keyring',
|
||||
f'--name=client.{self.app.name}',
|
||||
f'--add-key={self.ceph.key}']
|
||||
)
|
||||
except sunbeam_cprocess.ContainerProcessError:
|
||||
pass
|
||||
ph.init_service(self.contexts())
|
||||
|
||||
super().configure_charm(event)
|
||||
# Restarting services after bootstrap should be in aso
|
||||
if self._state.bootstrapped:
|
||||
for handler in self.pebble_handlers:
|
||||
handler.start_service()
|
||||
|
||||
|
||||
class GlanceWallabyOperatorCharm(GlanceOperatorCharm):
|
||||
|
||||
openstack_release = 'wallaby'
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Note: use_juju_for_storage=True required per
|
||||
# https://github.com/canonical/operator/issues/506
|
||||
main(GlanceWallabyOperatorCharm, use_juju_for_storage=True)
|
22
charms/glance-k8s/src/templates/ceph.conf.j2
Normal file
22
charms/glance-k8s/src/templates/ceph.conf.j2
Normal file
@@ -0,0 +1,22 @@
|
||||
###############################################################################
|
||||
# [ WARNING ]
|
||||
# ceph configuration file maintained in aso
|
||||
# local changes may be overwritten.
|
||||
###############################################################################
|
||||
[global]
|
||||
{% if ceph.auth -%}
|
||||
auth_supported = {{ ceph.auth }}
|
||||
mon host = {{ ceph.mon_hosts }}
|
||||
{% endif -%}
|
||||
keyring = /etc/ceph/$cluster.$name.keyring
|
||||
log to syslog = false
|
||||
err to syslog = false
|
||||
clog to syslog = false
|
||||
{% if ceph.rbd_features %}
|
||||
rbd default features = {{ ceph.rbd_features }}
|
||||
{% endif %}
|
||||
|
||||
[client]
|
||||
{% if ceph_config.rbd_default_data_pool -%}
|
||||
rbd default data pool = {{ ceph_config.rbd_default_data_pool }}
|
||||
{% endif %}
|
26
charms/glance-k8s/src/templates/cinder.conf.j2
Normal file
26
charms/glance-k8s/src/templates/cinder.conf.j2
Normal file
@@ -0,0 +1,26 @@
|
||||
###############################################################################
|
||||
# [ WARNING ]
|
||||
# cinder configuration file maintained by Juju
|
||||
# local changes may be overwritten.
|
||||
###############################################################################
|
||||
[DEFAULT]
|
||||
rootwrap_config = /etc/cinder/rootwrap.conf
|
||||
api_paste_confg = /etc/cinder/api-paste.ini
|
||||
debug = {{ options.debug }}
|
||||
transport_url = {{ amqp.transport_url }}
|
||||
auth_strategy = keystone
|
||||
|
||||
{% include "parts/section-database" %}
|
||||
|
||||
[keystone_authtoken]
|
||||
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
auth_type = password
|
||||
project_domain_name = {{ identity_service.service_domain_name }}
|
||||
user_domain_name = {{ identity_service.service_domain_name }}
|
||||
project_name = {{ identity_service.service_project_name }}
|
||||
username = {{ identity_service.service_user_name }}
|
||||
password = {{ identity_service.service_password }}
|
||||
|
||||
[oslo_concurrency]
|
||||
lock_path = /var/lib/cinder/tmp
|
28
charms/glance-k8s/src/templates/glance-api.conf.j2
Normal file
28
charms/glance-k8s/src/templates/glance-api.conf.j2
Normal file
@@ -0,0 +1,28 @@
|
||||
###############################################################################
|
||||
# [ WARNING ]
|
||||
# glance configuration file maintained by Juju
|
||||
# local changes may be overwritten.
|
||||
###############################################################################
|
||||
[DEFAULT]
|
||||
debug = {{ options.debug }}
|
||||
transport_url = {{ amqp.transport_url }}
|
||||
|
||||
{% include "parts/section-database" %}
|
||||
|
||||
{% include "parts/section-identity" %}
|
||||
|
||||
|
||||
|
||||
[glance_store]
|
||||
default_backend = ceph
|
||||
filesystem_store_datadir = /var/lib/glance/images/
|
||||
|
||||
[ceph]
|
||||
rbd_store_chunk_size = 8
|
||||
rbd_store_pool = glance
|
||||
rbd_store_user = glance
|
||||
rados_connect_timeout = 0
|
||||
rbd_store_ceph_conf = /etc/ceph/ceph.conf
|
||||
|
||||
[paste_deploy]
|
||||
flavor = keystone
|
7
charms/glance-k8s/src/templates/parts/section-database
Normal file
7
charms/glance-k8s/src/templates/parts/section-database
Normal file
@@ -0,0 +1,7 @@
|
||||
[database]
|
||||
{% if shared_db.database_host -%}
|
||||
connection = {{ shared_db.database_type }}://{{ shared_db.database_user }}:{{ shared_db.database_password }}@{{ shared_db.database_host }}/{{ shared_db.database }}{% if shared_db.database_ssl_ca %}?ssl_ca={{ shared_db.database_ssl_ca }}{% if shared_db.database_ssl_cert %}&ssl_cert={{ shared_db.database_ssl_cert }}&ssl_key={{ shared_db.database_ssl_key }}{% endif %}{% endif %}
|
||||
{% else -%}
|
||||
connection = sqlite:////var/lib/cinder/cinder.db
|
||||
{% endif -%}
|
||||
connection_recycle_time = 200
|
10
charms/glance-k8s/src/templates/parts/section-federation
Normal file
10
charms/glance-k8s/src/templates/parts/section-federation
Normal file
@@ -0,0 +1,10 @@
|
||||
{% if trusted_dashboards %}
|
||||
[federation]
|
||||
{% for dashboard_url in trusted_dashboards -%}
|
||||
trusted_dashboard = {{ dashboard_url }}
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
{% for sp in fid_sps -%}
|
||||
[{{ sp['protocol-name'] }}]
|
||||
remote_id_attribute = {{ sp['remote-id-attribute'] }}
|
||||
{% endfor -%}
|
11
charms/glance-k8s/src/templates/parts/section-identity
Normal file
11
charms/glance-k8s/src/templates/parts/section-identity
Normal file
@@ -0,0 +1,11 @@
|
||||
[keystone_authtoken]
|
||||
{% if identity_service.internal_host -%}
|
||||
www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }}
|
||||
auth_type = password
|
||||
project_domain_name = {{ identity_service.service_domain_name }}
|
||||
user_domain_name = {{ identity_service.service_domain_name }}
|
||||
project_name = {{ identity_service.service_project_name }}
|
||||
username = {{ identity_service.service_user_name }}
|
||||
password = {{ identity_service.service_password }}
|
||||
{% endif -%}
|
6
charms/glance-k8s/src/templates/parts/section-middleware
Normal file
6
charms/glance-k8s/src/templates/parts/section-middleware
Normal file
@@ -0,0 +1,6 @@
|
||||
{% for section in sections -%}
|
||||
[{{section}}]
|
||||
{% for key, value in sections[section].items() -%}
|
||||
{{ key }} = {{ value }}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
15
charms/glance-k8s/src/templates/parts/section-signing
Normal file
15
charms/glance-k8s/src/templates/parts/section-signing
Normal file
@@ -0,0 +1,15 @@
|
||||
{% if enable_signing -%}
|
||||
[signing]
|
||||
{% if certfile -%}
|
||||
certfile = {{ certfile }}
|
||||
{% endif -%}
|
||||
{% if keyfile -%}
|
||||
keyfile = {{ keyfile }}
|
||||
{% endif -%}
|
||||
{% if ca_certs -%}
|
||||
ca_certs = {{ ca_certs }}
|
||||
{% endif -%}
|
||||
{% if ca_key -%}
|
||||
ca_key = {{ ca_key }}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
25
charms/glance-k8s/src/templates/wsgi-cinder.conf.j2
Normal file
25
charms/glance-k8s/src/templates/wsgi-cinder.conf.j2
Normal file
@@ -0,0 +1,25 @@
|
||||
Listen 8776
|
||||
<VirtualHost *:8776>
|
||||
WSGIDaemonProcess cinder processes=3 threads=1 user=cinder group=cinder \
|
||||
display-name=%{GROUP}
|
||||
WSGIProcessGroup cinder
|
||||
WSGIScriptAlias / /usr/bin/cinder-wsgi
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
WSGIPassAuthorization On
|
||||
<IfVersion >= 2.4>
|
||||
ErrorLogFormat "%{cu}t %M"
|
||||
</IfVersion>
|
||||
ErrorLog /var/log/apache2/cinder_error.log
|
||||
CustomLog /var/log/apache2/cinder_access.log combined
|
||||
|
||||
<Directory /usr/bin>
|
||||
<IfVersion >= 2.4>
|
||||
Require all granted
|
||||
</IfVersion>
|
||||
<IfVersion < 2.4>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</IfVersion>
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
|
24
charms/glance-k8s/src/templates/wsgi-glance-api.conf.j2
Normal file
24
charms/glance-k8s/src/templates/wsgi-glance-api.conf.j2
Normal file
@@ -0,0 +1,24 @@
|
||||
Listen {{ wsgi_config.public_port }}
|
||||
<VirtualHost *:{{ wsgi_config.public_port }}>
|
||||
WSGIDaemonProcess glance processes=3 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \
|
||||
display-name=%{GROUP}
|
||||
WSGIProcessGroup glance
|
||||
WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }}
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
WSGIPassAuthorization On
|
||||
<IfVersion >= 2.4>
|
||||
ErrorLogFormat "%{cu}t %M"
|
||||
</IfVersion>
|
||||
ErrorLog {{ wsgi_config.error_log }}
|
||||
CustomLog {{ wsgi_config.custom_log }} combined
|
||||
|
||||
<Directory /usr/bin>
|
||||
<IfVersion >= 2.4>
|
||||
Require all granted
|
||||
</IfVersion>
|
||||
<IfVersion < 2.4>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</IfVersion>
|
||||
</Directory>
|
||||
</VirtualHost>
|
17
charms/glance-k8s/test-requirements.txt
Normal file
17
charms/glance-k8s/test-requirements.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
# This file is managed centrally. If you find the need to modify this as a
|
||||
# one-off, please don't. Intead, consult #openstack-charms and ask about
|
||||
# requirements management in charms via bot-control. Thank you.
|
||||
charm-tools>=2.4.4
|
||||
coverage>=3.6
|
||||
mock>=1.2
|
||||
flake8>=2.2.4,<=2.4.1
|
||||
pyflakes==2.1.1
|
||||
stestr>=2.2.0
|
||||
requests>=2.18.4
|
||||
psutil
|
||||
# oslo.i18n dropped py35 support
|
||||
oslo.i18n<4.0.0
|
||||
git+https://github.com/openstack-charmers/zaza.git#egg=zaza
|
||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
||||
pytz # workaround for 14.04 pip/tox
|
||||
pyudev # for ceph-* charm unit tests (not mocked?)
|
0
charms/glance-k8s/tests/__init__.py
Normal file
0
charms/glance-k8s/tests/__init__.py
Normal file
66
charms/glance-k8s/tests/test_charm.py
Normal file
66
charms/glance-k8s/tests/test_charm.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Copyright 2021 liam
|
||||
# See LICENSE file for licensing details.
|
||||
#
|
||||
# Learn more about testing at: https://juju.is/docs/sdk/testing
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from charm import SunbeamGlanceOperatorCharm
|
||||
from ops.model import ActiveStatus
|
||||
from ops.testing import Harness
|
||||
|
||||
|
||||
class TestCharm(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.harness = Harness(SunbeamGlanceOperatorCharm)
|
||||
self.addCleanup(self.harness.cleanup)
|
||||
self.harness.begin()
|
||||
|
||||
def test_config_changed(self):
|
||||
self.assertEqual(list(self.harness.charm._stored.things), [])
|
||||
self.harness.update_config({"thing": "foo"})
|
||||
self.assertEqual(list(self.harness.charm._stored.things), ["foo"])
|
||||
|
||||
def test_action(self):
|
||||
# the harness doesn't (yet!) help much with actions themselves
|
||||
action_event = Mock(params={"fail": ""})
|
||||
self.harness.charm._on_fortune_action(action_event)
|
||||
|
||||
self.assertTrue(action_event.set_results.called)
|
||||
|
||||
def test_action_fail(self):
|
||||
action_event = Mock(params={"fail": "fail this"})
|
||||
self.harness.charm._on_fortune_action(action_event)
|
||||
|
||||
self.assertEqual(action_event.fail.call_args, [("fail this",)])
|
||||
|
||||
def test_httpbin_pebble_ready(self):
|
||||
# Check the initial Pebble plan is empty
|
||||
initial_plan = self.harness.get_container_pebble_plan("httpbin")
|
||||
self.assertEqual(initial_plan.to_yaml(), "{}\n")
|
||||
# Expected plan after Pebble ready with default config
|
||||
expected_plan = {
|
||||
"services": {
|
||||
"httpbin": {
|
||||
"override": "replace",
|
||||
"summary": "httpbin",
|
||||
"command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent",
|
||||
"startup": "enabled",
|
||||
"environment": {"thing": "🎁"},
|
||||
}
|
||||
},
|
||||
}
|
||||
# Get the httpbin container from the model
|
||||
container = self.harness.model.unit.get_container("httpbin")
|
||||
# Emit the PebbleReadyEvent carrying the httpbin container
|
||||
self.harness.charm.on.httpbin_pebble_ready.emit(container)
|
||||
# Get the plan now we've run PebbleReady
|
||||
updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict()
|
||||
# Check we've got the plan we expected
|
||||
self.assertEqual(expected_plan, updated_plan)
|
||||
# Check the service was started
|
||||
service = self.harness.model.unit.get_container("httpbin").get_service("httpbin")
|
||||
self.assertTrue(service.is_running())
|
||||
# Ensure we set an ActiveStatus with no message
|
||||
self.assertEqual(self.harness.model.unit.status, ActiveStatus())
|
134
charms/glance-k8s/tox.ini
Normal file
134
charms/glance-k8s/tox.ini
Normal file
@@ -0,0 +1,134 @@
|
||||
# Operator charm (with zaza): tox.ini
|
||||
|
||||
[tox]
|
||||
envlist = pep8,py3
|
||||
skipsdist = True
|
||||
# NOTE: Avoid build/test env pollution by not enabling sitepackages.
|
||||
sitepackages = False
|
||||
# NOTE: Avoid false positives by not skipping missing interpreters.
|
||||
skip_missing_interpreters = False
|
||||
# NOTES:
|
||||
# * We avoid the new dependency resolver by pinning pip < 20.3, see
|
||||
# https://github.com/pypa/pip/issues/9187
|
||||
# * Pinning dependencies requires tox >= 3.2.0, see
|
||||
# https://tox.readthedocs.io/en/latest/config.html#conf-requires
|
||||
# * It is also necessary to pin virtualenv as a newer virtualenv would still
|
||||
# lead to fetching the latest pip in the func* tox targets, see
|
||||
# https://stackoverflow.com/a/38133283
|
||||
requires = pip < 20.3
|
||||
virtualenv < 20.0
|
||||
# NOTE: https://wiki.canonical.com/engineering/OpenStack/InstallLatestToxOnOsci
|
||||
minversion = 3.2.0
|
||||
|
||||
[testenv]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
PYTHONHASHSEED=0
|
||||
CHARM_DIR={envdir}
|
||||
install_command =
|
||||
pip install {opts} {packages}
|
||||
commands = stestr run --slowest {posargs}
|
||||
whitelist_externals =
|
||||
git
|
||||
add-to-archive.py
|
||||
bash
|
||||
charmcraft
|
||||
passenv = HOME TERM CS_* OS_* TEST_*
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:py35]
|
||||
basepython = python3.5
|
||||
# python3.5 is irrelevant on a focal+ charm.
|
||||
commands = /bin/true
|
||||
|
||||
[testenv:py36]
|
||||
basepython = python3.6
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:py37]
|
||||
basepython = python3.7
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:py38]
|
||||
basepython = python3.8
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:py3]
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:pep8]
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = flake8 {posargs} src unit_tests tests
|
||||
|
||||
[testenv:cover]
|
||||
# Technique based heavily upon
|
||||
# https://github.com/openstack/nova/blob/master/tox.ini
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run
|
||||
commands =
|
||||
coverage erase
|
||||
stestr run --slowest {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
concurrency = multiprocessing
|
||||
parallel = True
|
||||
source =
|
||||
.
|
||||
omit =
|
||||
.tox/*
|
||||
*/charmhelpers/*
|
||||
unit_tests/*
|
||||
|
||||
[testenv:venv]
|
||||
basepython = python3
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:build]
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/build-requirements.txt
|
||||
commands =
|
||||
charmcraft build
|
||||
|
||||
[testenv:func-noop]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --help
|
||||
|
||||
[testenv:func]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model
|
||||
|
||||
[testenv:func-smoke]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --smoke
|
||||
|
||||
[testenv:func-dev]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --dev
|
||||
|
||||
[testenv:func-target]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --bundle {posargs}
|
||||
|
||||
[flake8]
|
||||
# Ignore E902 because the unit_tests directory is missing in the built charm.
|
||||
ignore = E402,E226,E902
|
0
charms/glance-k8s/unit_tests/__init__.py
Normal file
0
charms/glance-k8s/unit_tests/__init__.py
Normal file
72
charms/glance-k8s/unit_tests/test_cinder_charm.py
Normal file
72
charms/glance-k8s/unit_tests/test_cinder_charm.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2021 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 sys
|
||||
|
||||
sys.path.append('lib') # noqa
|
||||
sys.path.append('src') # noqa
|
||||
|
||||
import charm
|
||||
import advanced_sunbeam_openstack.test_utils as test_utils
|
||||
|
||||
|
||||
class _GlanceWallabyOperatorCharm(charm.GlanceWallabyOperatorCharm):
|
||||
|
||||
def __init__(self, framework):
|
||||
self.seen_events = []
|
||||
self.render_calls = []
|
||||
super().__init__(framework)
|
||||
|
||||
def _log_event(self, event):
|
||||
self.seen_events.append(type(event).__name__)
|
||||
|
||||
def renderer(self, containers, container_configs, template_dir,
|
||||
openstack_release, adapters):
|
||||
self.render_calls.append(
|
||||
(
|
||||
containers,
|
||||
container_configs,
|
||||
template_dir,
|
||||
openstack_release,
|
||||
adapters))
|
||||
|
||||
def configure_charm(self, event):
|
||||
super().configure_charm(event)
|
||||
self._log_event(event)
|
||||
|
||||
|
||||
class TestGlanceOperatorCharm(test_utils.CharmTestCase):
|
||||
|
||||
PATCHES = [
|
||||
'KubernetesServicePatch'
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
self.container_calls = {
|
||||
'push': {},
|
||||
'pull': [],
|
||||
'remove_path': []}
|
||||
super().setUp(charm, self.PATCHES)
|
||||
self.harness = test_utils.get_harness(
|
||||
_GlanceWallabyOperatorCharm,
|
||||
container_calls=self.container_calls)
|
||||
self.addCleanup(self.harness.cleanup)
|
||||
self.harness.begin()
|
||||
|
||||
def test_pebble_ready_handler(self):
|
||||
self.assertEqual(self.harness.charm.seen_events, [])
|
||||
self.harness.container_pebble_ready('glance-api')
|
||||
self.assertEqual(self.harness.charm.seen_events, ['PebbleReadyEvent'])
|
Reference in New Issue
Block a user