Merge lp:~landscape/landscape-charm/trunk into lp:~landscape/landscape-charm/stable

Proposed by Bartosz Woronicz
Status: Superseded
Proposed branch: lp:~landscape/landscape-charm/trunk
Merge into: lp:~landscape/landscape-charm/stable
Diff against target: 9376 lines (+5057/-1599) (has conflicts)
84 files modified
.bzrignore (+1/-2)
HACKING.md (+44/-13)
Makefile (+30/-21)
README.md (+46/-28)
actions.yaml (+5/-5)
charm-helpers.yaml (+1/-0)
charmhelpers/__init__.py (+76/-17)
charmhelpers/contrib/__init__.py (+11/-13)
charmhelpers/contrib/hahelpers/__init__.py (+11/-13)
charmhelpers/contrib/hahelpers/apache.py (+24/-20)
charmhelpers/contrib/hahelpers/cluster.py (+113/-23)
charmhelpers/core/__init__.py (+11/-13)
charmhelpers/core/decorators.py (+11/-13)
charmhelpers/core/files.py (+11/-13)
charmhelpers/core/fstab.py (+11/-13)
charmhelpers/core/hookenv.py (+548/-36)
charmhelpers/core/host.py (+602/-166)
charmhelpers/core/host_factory/centos.py (+72/-0)
charmhelpers/core/host_factory/ubuntu.py (+114/-0)
charmhelpers/core/hugepage.py (+11/-13)
charmhelpers/core/kernel.py (+34/-30)
charmhelpers/core/kernel_factory/centos.py (+17/-0)
charmhelpers/core/kernel_factory/ubuntu.py (+13/-0)
charmhelpers/core/services/__init__.py (+11/-13)
charmhelpers/core/services/base.py (+29/-20)
charmhelpers/core/services/helpers.py (+11/-13)
charmhelpers/core/strutils.py (+75/-18)
charmhelpers/core/sysctl.py (+32/-23)
charmhelpers/core/templating.py (+37/-25)
charmhelpers/core/unitdata.py (+19/-15)
charmhelpers/fetch/__init__.py (+53/-302)
charmhelpers/fetch/archiveurl.py (+12/-14)
charmhelpers/fetch/bzrurl.py (+42/-48)
charmhelpers/fetch/centos.py (+171/-0)
charmhelpers/fetch/giturl.py (+33/-33)
charmhelpers/fetch/python/__init__.py (+13/-0)
charmhelpers/fetch/python/debug.py (+54/-0)
charmhelpers/fetch/python/packages.py (+154/-0)
charmhelpers/fetch/python/rpdb.py (+56/-0)
charmhelpers/fetch/python/version.py (+32/-0)
charmhelpers/fetch/snap.py (+150/-0)
charmhelpers/fetch/ubuntu.py (+730/-0)
charmhelpers/osplatform.py (+25/-0)
config.yaml (+34/-0)
dev/charm_helpers_sync.py (+32/-24)
dev/deployer (+29/-30)
dev/ubuntu-deps (+13/-10)
hooks/install (+1/-1)
icon.svg (+1/-288)
lib/action.py (+1/-1)
lib/apt.py (+60/-26)
lib/bootstrap.py (+21/-15)
lib/callbacks/scripts.py (+48/-2)
lib/callbacks/tests/test_apt.py (+1/-1)
lib/callbacks/tests/test_scripts.py (+43/-2)
lib/paths.py (+1/-0)
lib/relations/haproxy.py (+57/-6)
lib/relations/hosted.py (+61/-2)
lib/relations/postgresql.py (+30/-35)
lib/relations/tests/test_haproxy.py (+175/-14)
lib/relations/tests/test_hosted.py (+148/-8)
lib/relations/tests/test_postgresql.py (+18/-17)
lib/services.py (+7/-3)
lib/tests/rootdir.py (+1/-1)
lib/tests/sample.py (+4/-3)
lib/tests/stubs.py (+19/-2)
lib/tests/test_apt.py (+69/-28)
lib/tests/test_bootstrap.py (+46/-13)
lib/tests/test_install.py (+6/-5)
lib/tests/test_services.py (+16/-4)
lib/tests/test_templates.py (+95/-0)
lib/tests/test_upgrade.py (+67/-4)
lib/tests/test_utils.py (+150/-1)
lib/utils.py (+71/-0)
metadata.yaml (+4/-0)
templates/landscape-server (+4/-0)
templates/service.conf (+21/-0)
tests/basic/test_actions.py (+1/-1)
tests/basic/test_ha.py (+21/-21)
tests/basic/test_leader.py (+2/-4)
tests/basic/test_service.py (+20/-3)
tests/helpers.py (+90/-38)
tests/layers.py (+11/-5)
tests/test_helpers.py (+2/-5)
Text conflict in Makefile
Text conflict in README.md
Text conflict in config.yaml
Text conflict in tests/basic/test_service.py
To merge this branch: bzr merge lp:~landscape/landscape-charm/trunk
Reviewer Review Type Date Requested Status
Simon Poirier (community) Needs Resubmitting
Review via email: mp+401061@code.launchpad.net

This proposal has been superseded by a proposal from 2021-04-14.

Commit message

add oidc-* options

Description of the change

Added changes to support OpenID-Connect options.

To post a comment you must log in.
Revision history for this message
Simon Poirier (simpoir) wrote :

This doesn't seem to point to the right branches. This MP doesn't seem to contain anything related to oidc config.
Please branch from an up-to-date lp:landscape-charm and submit an MP of your branch targeting lp:landscape-charm.

review: Needs Resubmitting
lp:~landscape/landscape-charm/trunk updated
403. By Simon Poirier

Merge landscape-charm [f=1923863] [r=simpoir] [a=mastier1]
add oidc-* options

404. By Simon Poirier

Merge 1501803_ha_amqp [f=1501803] [r=maxiberta] [a=Simon Poirier]
Add all units to broker section with HA rabbitmq.

405. By Simon Poirier

Merge juju-deploy-err [f=] [r=maxiberta] [a=Simon Poirier]
Replace juju-deployer by plain "juju deploy" in integration tests to allow testing on bionic.

406. By Linda Guo <email address hidden>

Merge registration-relation [f=1945732] [r=landscape-builder,simpoir] [a=Linda Guo]
Add application-dashboard relation

  By adding application-dashboard relation in landscape charm,
  we can register landscape in Homer dashboard after
  creating relation between landscape charm and Homer charm

407. By Linda Guo <email address hidden>

Merge bug-1934816 [f=1934816] [r=landscape-builder,simpoir,elmo] [a=Linda Guo]
Sync charm-helpers
Add relation: nrpe-external-master
Add nrpe checks check_systemd for landscape services

408. By Simon Poirier

Merge 1682105_revisit_config [f=1499686,1682105] [r=landscape-builder,silverdrake11] [a=Simon Poirier]
Make proxy, system-email and root-url configurations updatable through the charm config.

Unmerged revisions

408. By Simon Poirier

Merge 1682105_revisit_config [f=1499686,1682105] [r=landscape-builder,silverdrake11] [a=Simon Poirier]
Make proxy, system-email and root-url configurations updatable through the charm config.

407. By Linda Guo <email address hidden>

Merge bug-1934816 [f=1934816] [r=landscape-builder,simpoir,elmo] [a=Linda Guo]
Sync charm-helpers
Add relation: nrpe-external-master
Add nrpe checks check_systemd for landscape services

406. By Linda Guo <email address hidden>

Merge registration-relation [f=1945732] [r=landscape-builder,simpoir] [a=Linda Guo]
Add application-dashboard relation

  By adding application-dashboard relation in landscape charm,
  we can register landscape in Homer dashboard after
  creating relation between landscape charm and Homer charm

405. By Simon Poirier

Merge juju-deploy-err [f=] [r=maxiberta] [a=Simon Poirier]
Replace juju-deployer by plain "juju deploy" in integration tests to allow testing on bionic.

404. By Simon Poirier

Merge 1501803_ha_amqp [f=1501803] [r=maxiberta] [a=Simon Poirier]
Add all units to broker section with HA rabbitmq.

403. By Simon Poirier

Merge landscape-charm [f=1923863] [r=simpoir] [a=mastier1]
add oidc-* options

402. By Simon Poirier

Merge pass_ping_through_https [f=1878265] [r=roadmr,landscape-builder] [a=Simon Poirier]
Add ping service to the https frontend in haproxy.

401. By Simon Poirier

Merge trivial_doc_updates [f=1846394,1864699] [r=landscape-builder,cjohnston,roadmr] [a=Simon Poirier]
Trivial documentation updates on config and actions.

400. By Simon Poirier

Update to 19.10 release PPA

399. By Guillermo Gonzalez

use postgresql-charm v2 protocol, manually parse the dsn as the available psycopg2 (2.6.x) doesn't provide parse_dsn (added in 2.7.x)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2015-07-09 09:00:17 +0000
+++ .bzrignore 2021-04-13 19:16:05 +0000
@@ -1,7 +1,6 @@
1_trial_temp1_trial_temp
2bundles2bundles
3config/license-file3config
4config/repo-file
5hooks/_trial_temp4hooks/_trial_temp
6tags5tags
7secrets6secrets
87
=== modified file 'HACKING.md'
--- HACKING.md 2014-03-04 11:36:06 +0000
+++ HACKING.md 2021-04-13 19:16:05 +0000
@@ -9,20 +9,51 @@
9Integration Testing9Integration Testing
10===================10===================
1111
12This charm comes with an integration test suite. You can run it as follows:12This charm comes with an integration test suite, which lives in the 'tests'
13directory. You can run it as follows:
1314
14 make integration-test15 make integration-test
1516
16N.B., It will deploy into a real juju environment. It uses the juju-test17N.B., It will deploy into a the current juju model. It uses the bundletester
17command to facilitate this, which takes care of bootstrapping for you. It18command to facilitate this. the `JUJU_MODEL` environment variable can be passed
18will use whatever 'juju env' reports as your current environment. It will use19to specify a different model. It will use a number of machines to do this test
19a number of machines to do this test -- it should work on a local environment20-- it should work on a local controller (LXD), but could be quite resource
20(LXC), but could be quite resource intensive.21intensive.
2122
22Please also note that if you want to deploy the current charm branch you are23
23working on, you need to change the branch URL in the landscape-deployments.yaml24Running parts of the integration tests
24file to point to your branch and commit all changes. After you are done with25--------------------------------------
25testing, remember to return the branch URL to what it was before26
27Running all the integration tests may take quite a while, and sometimes
28you want to just run the ones that you are working on. To do that, you
29can bootstrap the Juju environment yourself, and then use the zope
30testrunner directly. For example:
31
32 zope-testrunner3 -vv --path tests --tests-pattern basic --test some_test
33
34If the different services already are deployed, the command above is enough.
35But if you run it against an empty environment, you have to remember to pass
36along environment variables that affect the deployment, such as
37LS_CHARM_SOURCE=lds-trunk-ppa as to generate the secrets and bundles:
38
39 make secrets bundles
40
41
42Running integration tests on dense MAAS deployment
43--------------------------------------------------
44
45It's possible to run the integration tests on a dense MAAS deployment,
46where all the services run on the bootstrap node. But given that it's a
47bit different from other deployments, you have to explicitly tell that
48it's such a deployment using the DENSE_MAAS environment variable. For
49example:
50
51 DENSE_MAAS=1 make integration-test-trunk
52
53Or, if you already have the environment bootstrapped:
54
55 DENSE_MAAS=1 LS_CHARM_SOURCE=lds-trunk-ppa zope-testrunner3 -vv \
56 --path tests --tests-pattern basic --test some_test
2657
2758
28Testing with Dependent Upstream Charms59Testing with Dependent Upstream Charms
@@ -33,5 +64,5 @@
3364
34 make update-charm-revision-numbers65 make update-charm-revision-numbers
3566
36After running this, you can use bzr diff to see what (if any) changes were67After running this, you can use 'bzr diff bundles' to see what (if any)
37made to landscape-deployments.yaml.68changes were made to landscape-deployments.yaml.
3869
=== modified file 'Makefile'
--- Makefile 2016-02-10 22:05:36 +0000
+++ Makefile 2021-04-13 19:16:05 +0000
@@ -2,35 +2,45 @@
22
3test:3test:
4 trial lib4 trial lib
5 # For now only the install hook runs against python3
6 trial3 lib/tests/test_apt.py lib/tests/test_install.py
57
6ci-test:8ci-test:
7 ./dev/ubuntu-deps9 ./dev/ubuntu-deps
8 $(MAKE) test lint10 $(MAKE) test lint
911
10verify-juju-test:
11 @echo "Checking for ... "
12 @echo -n "juju-test: "
13 @if [ -z `which juju-test` ]; then \
14 echo -e "\nRun ./dev/ubuntu-deps to get the juju-test command installed"; \
15 exit 1;\
16 else \
17 echo "installed"; \
18 fi
19
20update-charm-revision-numbers: bundles12update-charm-revision-numbers: bundles
21 @dev/update-charm-revision-numbers \13 @dev/update-charm-revision-numbers \
22 $(EXTRA_UPDATE_ARGUMENTS) \14 $(EXTRA_UPDATE_ARGUMENTS) \
23 apache2 postgresql juju-gui haproxy rabbitmq-server nfs15 apache2 postgresql juju-gui haproxy rabbitmq-server nfs
2416
25test-depends: verify-juju-test bundles17test-depends: bundles
26 @cd tests && python3 test_helpers.py18 pip install --user bundletester juju-deployer
19 pip3 install --user amulet
20 cd tests && python3 test_helpers.py
2721
28bundles:22bundles-checkout:
29 @if [ -d bundles ]; then \23 @if [ -d bundles ]; then \
30 bzr up bundles; \24 bzr up bundles; \
31 else \25 else \
26<<<<<<< TREE
32 bzr co lp:~landscape/landscape-charm/bundles-stable bundles; \27 bzr co lp:~landscape/landscape-charm/bundles-stable bundles; \
33 fi28 fi
29=======
30 bzr co lp:landscape-bundles bundles; \
31 fi; \
32 make -C bundles deps
33 make -C bundles clean
34
35bundles: bundles-checkout
36 bundles/render-bundles
37
38bundles-local-branch: bundles-checkout
39 bundles/render-bundles --landscape-branch $(CURDIR)
40
41bundles-local-charm: bundles-checkout
42 bundles/render-bundles --landscape-charm $(CURDIR)
43>>>>>>> MERGE-SOURCE
3444
35secrets:45secrets:
36 @if [ -d secrets ]; then \46 @if [ -d secrets ]; then \
@@ -40,29 +50,29 @@
40 fi50 fi
4151
42integration-test: test-depends52integration-test: test-depends
43 juju test --set-e -p LS_CHARM_SOURCE,JUJU_HOME,JUJU_ENV,PG_MANUAL_TUNING -v --timeout 3000s53 python2 ~/.local/bin/bundletester -v -l DEBUG --skip-implicit -t .
4454
45# Run integration tests using the LDS package from the lds-trunk PPA55# Run integration tests using the LDS package from the lds-trunk PPA
46integration-test-trunk: secrets56integration-test-trunk: secrets
47 LS_CHARM_SOURCE=lds-trunk-ppa $(MAKE) $(subst -trunk,,$@)57 LS_CHARM_SOURCE=lds-trunk-ppa $(MAKE) $(subst -trunk,,$@)
4858
49deploy-dense-maas: bundles59deploy-dense-maas: bundles-local-branch config
50 ./dev/deployer dense-maas60 ./dev/deployer dense-maas
5161
52deploy-dense-maas-dev: bundles62deploy-dense-maas-dev: bundles-local-branch config repo-file-trunk
53 ./dev/deployer dense-maas --flags juju-debug63 ./dev/deployer dense-maas --flags juju-debug
5464
55deploy: bundles65deploy: bundles-local-branch
56 ./dev/deployer scalable66 ./dev/deployer scalable
5767
58repo-file-trunk: secrets68repo-file-trunk: secrets config
59 grep -e "^source:" secrets/lds-trunk-ppa | cut -f 2- -d " " > config/repo-file69 grep -e "^source:" secrets/lds-trunk-ppa | cut -f 2- -d " " > config/repo-file
6070
61lint:71lint:
62 flake8 --filename='*' hooks72 flake8 --filename='*' hooks
63 flake8 lib tests73 flake8 lib tests
64 pyflakes3 tests dev/update-charm-revision-numbers74 pyflakes3 tests dev/update-charm-revision-numbers
65 find . -name *.py -not -path "./old/*" -not -path "*/charmhelpers/*" -print0 | xargs -0 flake875 find . -name *.py -not -path "./old/*" -not -path "./build/*" -not -path "*/charmhelpers/*" -print0 | xargs -0 flake8
66 flake8 tests dev/update-charm-revision-numbers76 flake8 tests dev/update-charm-revision-numbers
6777
68clean:78clean:
@@ -83,8 +93,7 @@
8393
84dev/charm_helpers_sync.py:94dev/charm_helpers_sync.py:
85 @mkdir -p dev95 @mkdir -p dev
86 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \96 @curl https://git.launchpad.net/charm-helpers/plain/tools/charm_helpers_sync/charm_helpers_sync.py > dev/charm_helpers_sync.py
87 > dev/charm_helpers_sync.py
8897
89sync: dev/charm_helpers_sync.py98sync: dev/charm_helpers_sync.py
90 $(PYTHON) dev/charm_helpers_sync.py -c charm-helpers.yaml99 $(PYTHON) dev/charm_helpers_sync.py -c charm-helpers.yaml
91100
=== modified file 'README.md'
--- README.md 2015-10-14 14:56:41 +0000
+++ README.md 2021-04-13 19:16:05 +0000
@@ -1,43 +1,37 @@
1Overview1Overview
2========2========
33The Landscape systems management tool helps you monitor, manage and update your entire Ubuntu infrastructure from a single interface. Part of Canonical's Ubuntu Advantage support service, Landscape brings you intuitive systems management tools combined with world-class support.
4The Landscape systems management tool helps you monitor, manage and update your4
5entire Ubuntu infrastructure from a single interface. Part of Canonical's5This charm will deploy Landscape On Premises, and needs to be connected to other charms to be fully functional. Example deployments are given below.
6Ubuntu Advantage support service, Landscape brings you intuitive systems
7management tools combined with world-class support.
8
9This charm will deploy Landscape Dedicated Server (LDS), and needs to be
10connected to other charms to be fully functional. Example deployments are given
11below.
126
13For more information about Landscape, go to http://www.ubuntu.com/management7For more information about Landscape, go to http://www.ubuntu.com/management
148
15Standard Usage9Standard Usage
16==============10==============
1711
18The typical deployment of Landscape happens using a Juju bundle. This charm is12The typical deployment of Landscape happens using a Juju bundle. This charm is not useful without a deployed bundle of services.
19not useful without a deployed bundle of services.
2013
21Please use one of the following bundle types depending on your needs:14Please use one of the following bundle types depending on your needs:
2215
16<<<<<<< TREE
23 * [landscape-scalable](https://jujucharms.com/u/landscape/landscape-scalable/)17 * [landscape-scalable](https://jujucharms.com/u/landscape/landscape-scalable/)
24 * [landscape-dense-maas](https://jujucharms.com/u/landscape/landscape-dense-maas/)18 * [landscape-dense-maas](https://jujucharms.com/u/landscape/landscape-dense-maas/)
25 * [landscape-dense](https://jujucharms.com/u/landscape/landscape-dense/)19 * [landscape-dense](https://jujucharms.com/u/landscape/landscape-dense/)
20=======
21* https://jujucharms.com/landscape-scalable/
22* https://jujucharms.com/landscape-dense-maas/
23* https://jujucharms.com/landscape-dense/
24>>>>>>> MERGE-SOURCE
2625
27For the landscape-scalable case:26For the landscape-scalable case:
2827
29 sudo apt-add-repository ppa:juju/stable28 $ juju deploy landscape-scalable
30 sudo apt-get update29
31 juju quickstart u/landscape/landscape-scalable30
3231Customised Deployments
33
34Customized Deployments
35======================32======================
3633
37The standard deployment of Landscape will give you the latest released code.34The standard deployment of Landscape will give you the latest released code. If you want a different version, different options, etc, you will need to download one of the bundles, and add/change options in the file before supplying it to juju.
38If you want a different version, different options, etc, you will need to
39download one of the bundles, and add/change options in the file before
40supplying it to juju quickstart.
4135
42On the bundle page, download the `bundle.yaml` file.36On the bundle page, download the `bundle.yaml` file.
4337
@@ -45,6 +39,7 @@
45Configuration39Configuration
46=============40=============
4741
42<<<<<<< TREE
48Landscape is a commercial product which is bundled with a license43Landscape is a commercial product which is bundled with a license
49allowing management of up to 10 physical machines and 50 more LXC44allowing management of up to 10 physical machines and 50 more LXC
50containers, for a total of 60 seats.45containers, for a total of 60 seats.
@@ -55,34 +50,44 @@
55gather these details after purchasing seats for LDS. All information50gather these details after purchasing seats for LDS. All information
56is found by following a link on the left side of the page called51is found by following a link on the left side of the page called
57"access the Landscape Dedicated Server archive"52"access the Landscape Dedicated Server archive"
53=======
54Landscape is a commercial product and as such it needs configuration of a license and password protected repository before deployment. Please login to your "hosted account" (on landscape.canonical.com) to gather these details after purchasing seats for Landscape On Premises. All information is found by following a link on the left side of the page called "access the Landscape On Premises archive"
55>>>>>>> MERGE-SOURCE
5856
59license-file57license-file
60------------58------------
6159
60<<<<<<< TREE
62You can set this as a Juju configuration option after deployment61You can set this as a Juju configuration option after deployment
63on the landscape service like this:62on the landscape service like this:
63=======
64You can set this as a juju configuration option after deployment on each deployed landscape-server application like:
65>>>>>>> MERGE-SOURCE
6466
67<<<<<<< TREE
65 $ juju set landscape-server "license-file=$(cat license-file)"68 $ juju set landscape-server "license-file=$(cat license-file)"
69=======
70 $ juju config landscape-server "license-file=$(cat license-file)"
71>>>>>>> MERGE-SOURCE
6672
6773
68SSL74SSL
69===75===
7076
71The pre-packaged bundles will ask the HAProxy charm to generate a self77The pre-packaged bundles will ask the HAProxy charm to generate a self signed certificate. While useful for testing, this must not be used for production deployments.
72signed certificate. While useful for testing, this must not be used for
73production deployments.
7478
75For production deployments, you should include a "real" SSL certificate key79For production deployments, you should include a "real" SSL certificate key pair that has been signed by a CA that your clients trust in the haproxy service configuration (or in the landscape-server service configuration if you need to use your haproxy service for other services too with different certificates).
76pair that has been signed by a CA that your clients trust in the haproxy service
77configuration (or in the landscape-server service configuration if you need to
78use your haproxy service for other services too with different certificates).
7980
8081
81Unit Testing82Unit Testing
82============83============
8384
85<<<<<<< TREE
84The Landscape charm is unit tested and new code changes should be86The Landscape charm is unit tested and new code changes should be
85submitted with unit tests. You can run them like this:87submitted with unit tests. You can run them like this:
88=======
89The Landscape charm is fairly well unit tested and new code changes should be submitted with unit tests. You can run them like this:
90>>>>>>> MERGE-SOURCE
8691
87 $ make test92 $ make test
8893
@@ -90,6 +95,7 @@
90Integration Testing95Integration Testing
91===================96===================
9297
98<<<<<<< TREE
93This charm makes use of99This charm makes use of
94[juju-deployer](http://pythonhosted.org/juju-deployer/) and100[juju-deployer](http://pythonhosted.org/juju-deployer/) and
95[charm-tools](https://jujucharms.com/docs/1.20/tools-charm-tools) to101[charm-tools](https://jujucharms.com/docs/1.20/tools-charm-tools) to
@@ -105,3 +111,15 @@
105111
106The JUJU_ENV environment variable can be omitted if you want to use the112The JUJU_ENV environment variable can be omitted if you want to use the
107current juju environment (as set by "juju switch").113current juju environment (as set by "juju switch").
114=======
115This charm makes use of juju-deployer and the charm-tools package to enable end-to-end integration testing. This is how you proceed with running them:
116
117 $ juju bootstrap localhost
118 $ make integration-test
119
120Or if you want to use the packages from the lds-trunk PPA:
121
122 $ JUJU_MODEL=<model> make integration-test-trunk
123
124The JUJU_MODEL environment variable can be omitted if you want to use the current model.
125>>>>>>> MERGE-SOURCE
108126
=== modified file 'actions.yaml'
--- actions.yaml 2015-06-25 16:01:22 +0000
+++ actions.yaml 2021-04-13 19:16:05 +0000
@@ -1,10 +1,7 @@
1pause:1pause:
2 description: Pause the Landscape unit. This action will interrupt any2 description: Pause the Landscape service.
3 Landscape-related processing on the unit and prevent any further processing
4 from happening.
5resume:3resume:
6 description: Resume the Landscape unit. This action will start all the4 description: Resume the Landscape service.
7 Landscape services on the unit.
8upgrade:5upgrade:
9 description: Upgrade software on the Landscape unit. This action will update6 description: Upgrade software on the Landscape unit. This action will update
10 APT package indexes and upgrade the landscape-server package.7 APT package indexes and upgrade the landscape-server package.
@@ -25,5 +22,8 @@
25 admin-password:22 admin-password:
26 type: string23 type: string
27 description: Password for the administrator to add.24 description: Password for the administrator to add.
25 registration-key:
26 type: string
27 description: Registration key to set on the account.
28 required: [admin-name, admin-email, admin-password]28 required: [admin-name, admin-email, admin-password]
29 additionalProperties: false29 additionalProperties: false
3030
=== modified file 'charm-helpers.yaml'
--- charm-helpers.yaml 2015-05-07 10:26:45 +0000
+++ charm-helpers.yaml 2021-04-13 19:16:05 +0000
@@ -4,4 +4,5 @@
4 - __init__4 - __init__
5 - core5 - core
6 - fetch6 - fetch
7 - osplatform
7 - contrib.hahelpers8 - contrib.hahelpers
89
=== modified file 'charmhelpers/__init__.py'
--- charmhelpers/__init__.py 2015-01-28 08:59:02 +0000
+++ charmhelpers/__init__.py 2021-04-13 19:16:05 +0000
@@ -1,38 +1,97 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17# Bootstrap charm-helpers, installing its dependencies if necessary using15# Bootstrap charm-helpers, installing its dependencies if necessary using
18# only standard libraries.16# only standard libraries.
17from __future__ import print_function
18from __future__ import absolute_import
19
20import functools
21import inspect
19import subprocess22import subprocess
20import sys23import sys
2124
22try:25try:
23 import six # flake8: noqa26 import six # NOQA:F401
24except ImportError:27except ImportError:
25 if sys.version_info.major == 2:28 if sys.version_info.major == 2:
26 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])29 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
27 else:30 else:
28 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])31 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
29 import six # flake8: noqa32 import six # NOQA:F401
3033
31try:34try:
32 import yaml # flake8: noqa35 import yaml # NOQA:F401
33except ImportError:36except ImportError:
34 if sys.version_info.major == 2:37 if sys.version_info.major == 2:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])38 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
36 else:39 else:
37 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])40 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
38 import yaml # flake8: noqa41 import yaml # NOQA:F401
42
43
44# Holds a list of mapping of mangled function names that have been deprecated
45# using the @deprecate decorator below. This is so that the warning is only
46# printed once for each usage of the function.
47__deprecated_functions = {}
48
49
50def deprecate(warning, date=None, log=None):
51 """Add a deprecation warning the first time the function is used.
52 The date, which is a string in semi-ISO8660 format indicate the year-month
53 that the function is officially going to be removed.
54
55 usage:
56
57 @deprecate('use core/fetch/add_source() instead', '2017-04')
58 def contributed_add_source_thing(...):
59 ...
60
61 And it then prints to the log ONCE that the function is deprecated.
62 The reason for passing the logging function (log) is so that hookenv.log
63 can be used for a charm if needed.
64
65 :param warning: String to indicat where it has moved ot.
66 :param date: optional sting, in YYYY-MM format to indicate when the
67 function will definitely (probably) be removed.
68 :param log: The log function to call to log. If not, logs to stdout
69 """
70 def wrap(f):
71
72 @functools.wraps(f)
73 def wrapped_f(*args, **kwargs):
74 try:
75 module = inspect.getmodule(f)
76 file = inspect.getsourcefile(f)
77 lines = inspect.getsourcelines(f)
78 f_name = "{}-{}-{}..{}-{}".format(
79 module.__name__, file, lines[0], lines[-1], f.__name__)
80 except (IOError, TypeError):
81 # assume it was local, so just use the name of the function
82 f_name = f.__name__
83 if f_name not in __deprecated_functions:
84 __deprecated_functions[f_name] = True
85 s = "DEPRECATION WARNING: Function {} is being removed".format(
86 f.__name__)
87 if date:
88 s = "{} on/around {}".format(s, date)
89 if warning:
90 s = "{} : {}".format(s, warning)
91 if log:
92 log(s)
93 else:
94 print(s)
95 return f(*args, **kwargs)
96 return wrapped_f
97 return wrap
3998
=== modified file 'charmhelpers/contrib/__init__.py'
--- charmhelpers/contrib/__init__.py 2015-01-30 11:16:09 +0000
+++ charmhelpers/contrib/__init__.py 2021-04-13 19:16:05 +0000
@@ -1,15 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
=== modified file 'charmhelpers/contrib/hahelpers/__init__.py'
--- charmhelpers/contrib/hahelpers/__init__.py 2015-01-30 11:16:09 +0000
+++ charmhelpers/contrib/hahelpers/__init__.py 2021-04-13 19:16:05 +0000
@@ -1,15 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
=== modified file 'charmhelpers/contrib/hahelpers/apache.py'
--- charmhelpers/contrib/hahelpers/apache.py 2015-01-30 11:16:09 +0000
+++ charmhelpers/contrib/hahelpers/apache.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17#15#
18# Copyright 2012 Canonical Ltd.16# Copyright 2012 Canonical Ltd.
@@ -24,8 +22,9 @@
24# Adam Gandelman <adamg@ubuntu.com>22# Adam Gandelman <adamg@ubuntu.com>
25#23#
2624
27import subprocess25import os
2826
27from charmhelpers.core import host
29from charmhelpers.core.hookenv import (28from charmhelpers.core.hookenv import (
30 config as config_get,29 config as config_get,
31 relation_get,30 relation_get,
@@ -66,7 +65,8 @@
66 if ca_cert is None:65 if ca_cert is None:
67 log("Inspecting identity-service relations for CA SSL certificate.",66 log("Inspecting identity-service relations for CA SSL certificate.",
68 level=INFO)67 level=INFO)
69 for r_id in relation_ids('identity-service'):68 for r_id in (relation_ids('identity-service') +
69 relation_ids('identity-credentials')):
70 for unit in relation_list(r_id):70 for unit in relation_list(r_id):
71 if ca_cert is None:71 if ca_cert is None:
72 ca_cert = relation_get('ca_cert',72 ca_cert = relation_get('ca_cert',
@@ -74,9 +74,13 @@
74 return ca_cert74 return ca_cert
7575
7676
77def retrieve_ca_cert(cert_file):
78 cert = None
79 if os.path.isfile(cert_file):
80 with open(cert_file, 'rb') as crt:
81 cert = crt.read()
82 return cert
83
84
77def install_ca_cert(ca_cert):85def install_ca_cert(ca_cert):
78 if ca_cert:86 host.install_ca_cert(ca_cert, 'keystone_juju_ca_cert')
79 with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
80 'w') as crt:
81 crt.write(ca_cert)
82 subprocess.check_call(['update-ca-certificates', '--fresh'])
8387
=== modified file 'charmhelpers/contrib/hahelpers/cluster.py'
--- charmhelpers/contrib/hahelpers/cluster.py 2015-07-03 09:13:26 +0000
+++ charmhelpers/contrib/hahelpers/cluster.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17#15#
18# Copyright 2012 Canonical Ltd.16# Copyright 2012 Canonical Ltd.
@@ -29,6 +27,7 @@
2927
30import subprocess28import subprocess
31import os29import os
30import time
3231
33from socket import gethostname as get_unit_hostname32from socket import gethostname as get_unit_hostname
3433
@@ -41,10 +40,14 @@
41 relation_get,40 relation_get,
42 config as config_get,41 config as config_get,
43 INFO,42 INFO,
44 ERROR,43 DEBUG,
45 WARNING,44 WARNING,
46 unit_get,45 unit_get,
47 is_leader as juju_is_leader46 is_leader as juju_is_leader,
47 status_set,
48)
49from charmhelpers.core.host import (
50 modulo_distribution,
48)51)
49from charmhelpers.core.decorators import (52from charmhelpers.core.decorators import (
50 retry_on_exception,53 retry_on_exception,
@@ -60,6 +63,10 @@
60 pass63 pass
6164
6265
66class HAIncorrectConfig(Exception):
67 pass
68
69
63class CRMResourceNotFound(Exception):70class CRMResourceNotFound(Exception):
64 pass71 pass
6572
@@ -216,6 +223,11 @@
216 return True223 return True
217 if config_get('ssl_cert') and config_get('ssl_key'):224 if config_get('ssl_cert') and config_get('ssl_key'):
218 return True225 return True
226 for r_id in relation_ids('certificates'):
227 for unit in relation_list(r_id):
228 ca = relation_get('ca', rid=r_id, unit=unit)
229 if ca:
230 return True
219 for r_id in relation_ids('identity-service'):231 for r_id in relation_ids('identity-service'):
220 for unit in relation_list(r_id):232 for unit in relation_list(r_id):
221 # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN233 # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
@@ -274,27 +286,71 @@
274 Obtains all relevant configuration from charm configuration required286 Obtains all relevant configuration from charm configuration required
275 for initiating a relation to hacluster:287 for initiating a relation to hacluster:
276288
277 ha-bindiface, ha-mcastport, vip289 ha-bindiface, ha-mcastport, vip, os-internal-hostname,
290 os-admin-hostname, os-public-hostname, os-access-hostname
278291
279 param: exclude_keys: list of setting key(s) to be excluded.292 param: exclude_keys: list of setting key(s) to be excluded.
280 returns: dict: A dict containing settings keyed by setting name.293 returns: dict: A dict containing settings keyed by setting name.
281 raises: HAIncompleteConfig if settings are missing.294 raises: HAIncompleteConfig if settings are missing or incorrect.
282 '''295 '''
283 settings = ['ha-bindiface', 'ha-mcastport', 'vip']296 settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'os-internal-hostname',
297 'os-admin-hostname', 'os-public-hostname', 'os-access-hostname']
284 conf = {}298 conf = {}
285 for setting in settings:299 for setting in settings:
286 if exclude_keys and setting in exclude_keys:300 if exclude_keys and setting in exclude_keys:
287 continue301 continue
288302
289 conf[setting] = config_get(setting)303 conf[setting] = config_get(setting)
290 missing = []304
291 [missing.append(s) for s, v in six.iteritems(conf) if v is None]305 if not valid_hacluster_config():
292 if missing:306 raise HAIncorrectConfig('Insufficient or incorrect config data to '
293 log('Insufficient config data to configure hacluster.', level=ERROR)307 'configure hacluster.')
294 raise HAIncompleteConfig
295 return conf308 return conf
296309
297310
311def valid_hacluster_config():
312 '''
313 Check that either vip or dns-ha is set. If dns-ha then one of os-*-hostname
314 must be set.
315
316 Note: ha-bindiface and ha-macastport both have defaults and will always
317 be set. We only care that either vip or dns-ha is set.
318
319 :returns: boolean: valid config returns true.
320 raises: HAIncompatibileConfig if settings conflict.
321 raises: HAIncompleteConfig if settings are missing.
322 '''
323 vip = config_get('vip')
324 dns = config_get('dns-ha')
325 if not(bool(vip) ^ bool(dns)):
326 msg = ('HA: Either vip or dns-ha must be set but not both in order to '
327 'use high availability')
328 status_set('blocked', msg)
329 raise HAIncorrectConfig(msg)
330
331 # If dns-ha then one of os-*-hostname must be set
332 if dns:
333 dns_settings = ['os-internal-hostname', 'os-admin-hostname',
334 'os-public-hostname', 'os-access-hostname']
335 # At this point it is unknown if one or all of the possible
336 # network spaces are in HA. Validate at least one is set which is
337 # the minimum required.
338 for setting in dns_settings:
339 if config_get(setting):
340 log('DNS HA: At least one hostname is set {}: {}'
341 ''.format(setting, config_get(setting)),
342 level=DEBUG)
343 return True
344
345 msg = ('DNS HA: At least one os-*-hostname(s) must be set to use '
346 'DNS HA')
347 status_set('blocked', msg)
348 raise HAIncompleteConfig(msg)
349
350 log('VIP HA: VIP is set {}'.format(vip), level=DEBUG)
351 return True
352
353
298def canonical_url(configs, vip_setting='vip'):354def canonical_url(configs, vip_setting='vip'):
299 '''355 '''
300 Returns the correct HTTP URL to this host given the state of HTTPS356 Returns the correct HTTP URL to this host given the state of HTTPS
@@ -314,3 +370,37 @@
314 else:370 else:
315 addr = unit_get('private-address')371 addr = unit_get('private-address')
316 return '%s://%s' % (scheme, addr)372 return '%s://%s' % (scheme, addr)
373
374
375def distributed_wait(modulo=None, wait=None, operation_name='operation'):
376 ''' Distribute operations by waiting based on modulo_distribution
377
378 If modulo and or wait are not set, check config_get for those values.
379 If config values are not set, default to modulo=3 and wait=30.
380
381 :param modulo: int The modulo number creates the group distribution
382 :param wait: int The constant time wait value
383 :param operation_name: string Operation name for status message
384 i.e. 'restart'
385 :side effect: Calls config_get()
386 :side effect: Calls log()
387 :side effect: Calls status_set()
388 :side effect: Calls time.sleep()
389 '''
390 if modulo is None:
391 modulo = config_get('modulo-nodes') or 3
392 if wait is None:
393 wait = config_get('known-wait') or 30
394 if juju_is_leader():
395 # The leader should never wait
396 calculated_wait = 0
397 else:
398 # non_zero_wait=True guarantees the non-leader who gets modulo 0
399 # will still wait
400 calculated_wait = modulo_distribution(modulo=modulo, wait=wait,
401 non_zero_wait=True)
402 msg = "Waiting {} seconds for {} ...".format(calculated_wait,
403 operation_name)
404 log(msg, DEBUG)
405 status_set('maintenance', msg)
406 time.sleep(calculated_wait)
317407
=== modified file 'charmhelpers/core/__init__.py'
--- charmhelpers/core/__init__.py 2015-01-28 08:59:02 +0000
+++ charmhelpers/core/__init__.py 2021-04-13 19:16:05 +0000
@@ -1,15 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
=== modified file 'charmhelpers/core/decorators.py'
--- charmhelpers/core/decorators.py 2015-01-28 08:59:02 +0000
+++ charmhelpers/core/decorators.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17#15#
18# Copyright 2014 Canonical Ltd.16# Copyright 2014 Canonical Ltd.
1917
=== modified file 'charmhelpers/core/files.py'
--- charmhelpers/core/files.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/files.py 2021-04-13 19:16:05 +0000
@@ -3,19 +3,17 @@
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1917
20__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'18__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
2119
2220
=== modified file 'charmhelpers/core/fstab.py'
--- charmhelpers/core/fstab.py 2015-03-12 11:42:26 +0000
+++ charmhelpers/core/fstab.py 2021-04-13 19:16:05 +0000
@@ -3,19 +3,17 @@
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1917
20import io18import io
21import os19import os
2220
=== modified file 'charmhelpers/core/hookenv.py'
--- charmhelpers/core/hookenv.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/hookenv.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17"Interactions with the Juju environment"15"Interactions with the Juju environment"
18# Copyright 2013 Canonical Ltd.16# Copyright 2013 Canonical Ltd.
@@ -24,10 +22,12 @@
24import copy22import copy
25from distutils.version import LooseVersion23from distutils.version import LooseVersion
26from functools import wraps24from functools import wraps
25from collections import namedtuple
27import glob26import glob
28import os27import os
29import json28import json
30import yaml29import yaml
30import re
31import subprocess31import subprocess
32import sys32import sys
33import errno33import errno
@@ -40,12 +40,20 @@
40else:40else:
41 from collections import UserDict41 from collections import UserDict
4242
43
43CRITICAL = "CRITICAL"44CRITICAL = "CRITICAL"
44ERROR = "ERROR"45ERROR = "ERROR"
45WARNING = "WARNING"46WARNING = "WARNING"
46INFO = "INFO"47INFO = "INFO"
47DEBUG = "DEBUG"48DEBUG = "DEBUG"
49TRACE = "TRACE"
48MARKER = object()50MARKER = object()
51SH_MAX_ARG = 131071
52
53
54RANGE_WARNING = ('Passing NO_PROXY string that includes a cidr. '
55 'This may not be compatible with software you are '
56 'running in your shell.')
4957
50cache = {}58cache = {}
5159
@@ -66,7 +74,7 @@
66 @wraps(func)74 @wraps(func)
67 def wrapper(*args, **kwargs):75 def wrapper(*args, **kwargs):
68 global cache76 global cache
69 key = str((func, args, kwargs))77 key = json.dumps((func, args, kwargs), sort_keys=True, default=str)
70 try:78 try:
71 return cache[key]79 return cache[key]
72 except KeyError:80 except KeyError:
@@ -96,7 +104,7 @@
96 command += ['-l', level]104 command += ['-l', level]
97 if not isinstance(message, six.string_types):105 if not isinstance(message, six.string_types):
98 message = repr(message)106 message = repr(message)
99 command += [message]107 command += [message[:SH_MAX_ARG]]
100 # Missing juju-log should not cause failures in unit tests108 # Missing juju-log should not cause failures in unit tests
101 # Send log output to stderr109 # Send log output to stderr
102 try:110 try:
@@ -199,9 +207,56 @@
199 return os.environ.get('JUJU_REMOTE_UNIT', None)207 return os.environ.get('JUJU_REMOTE_UNIT', None)
200208
201209
210def application_name():
211 """
212 The name of the deployed application this unit belongs to.
213 """
214 return local_unit().split('/')[0]
215
216
202def service_name():217def service_name():
203 """The name service group this unit belongs to"""218 """
204 return local_unit().split('/')[0]219 .. deprecated:: 0.19.1
220 Alias for :func:`application_name`.
221 """
222 return application_name()
223
224
225def model_name():
226 """
227 Name of the model that this unit is deployed in.
228 """
229 return os.environ['JUJU_MODEL_NAME']
230
231
232def model_uuid():
233 """
234 UUID of the model that this unit is deployed in.
235 """
236 return os.environ['JUJU_MODEL_UUID']
237
238
239def principal_unit():
240 """Returns the principal unit of this unit, otherwise None"""
241 # Juju 2.2 and above provides JUJU_PRINCIPAL_UNIT
242 principal_unit = os.environ.get('JUJU_PRINCIPAL_UNIT', None)
243 # If it's empty, then this unit is the principal
244 if principal_unit == '':
245 return os.environ['JUJU_UNIT_NAME']
246 elif principal_unit is not None:
247 return principal_unit
248 # For Juju 2.1 and below, let's try work out the principle unit by
249 # the various charms' metadata.yaml.
250 for reltype in relation_types():
251 for rid in relation_ids(reltype):
252 for unit in related_units(rid):
253 md = _metadata_unit(unit)
254 if not md:
255 continue
256 subordinate = md.pop('subordinate', None)
257 if not subordinate:
258 return unit
259 return None
205260
206261
207@cached262@cached
@@ -265,7 +320,7 @@
265 self.implicit_save = True320 self.implicit_save = True
266 self._prev_dict = None321 self._prev_dict = None
267 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)322 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
268 if os.path.exists(self.path):323 if os.path.exists(self.path) and os.stat(self.path).st_size:
269 self.load_previous()324 self.load_previous()
270 atexit(self._implicit_save)325 atexit(self._implicit_save)
271326
@@ -285,7 +340,11 @@
285 """340 """
286 self.path = path or self.path341 self.path = path or self.path
287 with open(self.path) as f:342 with open(self.path) as f:
288 self._prev_dict = json.load(f)343 try:
344 self._prev_dict = json.load(f)
345 except ValueError as e:
346 log('Unable to parse previous config data - {}'.format(str(e)),
347 level=ERROR)
289 for k, v in copy.deepcopy(self._prev_dict).items():348 for k, v in copy.deepcopy(self._prev_dict).items():
290 if k not in self:349 if k not in self:
291 self[k] = v350 self[k] = v
@@ -321,6 +380,7 @@
321380
322 """381 """
323 with open(self.path, 'w') as f:382 with open(self.path, 'w') as f:
383 os.fchmod(f.fileno(), 0o600)
324 json.dump(self, f)384 json.dump(self, f)
325385
326 def _implicit_save(self):386 def _implicit_save(self):
@@ -328,20 +388,40 @@
328 self.save()388 self.save()
329389
330390
331@cached391_cache_config = None
392
393
332def config(scope=None):394def config(scope=None):
333 """Juju charm configuration"""395 """
334 config_cmd_line = ['config-get']396 Get the juju charm configuration (scope==None) or individual key,
335 if scope is not None:397 (scope=str). The returned value is a Python data structure loaded as
336 config_cmd_line.append(scope)398 JSON from the Juju config command.
337 config_cmd_line.append('--format=json')399
338 try:400 :param scope: If set, return the value for the specified key.
339 config_data = json.loads(401 :type scope: Optional[str]
340 subprocess.check_output(config_cmd_line).decode('UTF-8'))402 :returns: Either the whole config as a Config, or a key from it.
403 :rtype: Any
404 """
405 global _cache_config
406 config_cmd_line = ['config-get', '--all', '--format=json']
407 try:
408 # JSON Decode Exception for Python3.5+
409 exc_json = json.decoder.JSONDecodeError
410 except AttributeError:
411 # JSON Decode Exception for Python2.7 through Python3.4
412 exc_json = ValueError
413 try:
414 if _cache_config is None:
415 config_data = json.loads(
416 subprocess.check_output(config_cmd_line).decode('UTF-8'))
417 _cache_config = Config(config_data)
341 if scope is not None:418 if scope is not None:
342 return config_data419 return _cache_config.get(scope)
343 return Config(config_data)420 return _cache_config
344 except ValueError:421 except (exc_json, UnicodeDecodeError) as e:
422 log('Unable to parse output from config-get: config_cmd_line="{}" '
423 'message="{}"'
424 .format(config_cmd_line, str(e)), level=ERROR)
345 return None425 return None
346426
347427
@@ -435,6 +515,67 @@
435 subprocess.check_output(units_cmd_line).decode('UTF-8')) or []515 subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
436516
437517
518def expected_peer_units():
519 """Get a generator for units we expect to join peer relation based on
520 goal-state.
521
522 The local unit is excluded from the result to make it easy to gauge
523 completion of all peers joining the relation with existing hook tools.
524
525 Example usage:
526 log('peer {} of {} joined peer relation'
527 .format(len(related_units()),
528 len(list(expected_peer_units()))))
529
530 This function will raise NotImplementedError if used with juju versions
531 without goal-state support.
532
533 :returns: iterator
534 :rtype: types.GeneratorType
535 :raises: NotImplementedError
536 """
537 if not has_juju_version("2.4.0"):
538 # goal-state first appeared in 2.4.0.
539 raise NotImplementedError("goal-state")
540 _goal_state = goal_state()
541 return (key for key in _goal_state['units']
542 if '/' in key and key != local_unit())
543
544
545def expected_related_units(reltype=None):
546 """Get a generator for units we expect to join relation based on
547 goal-state.
548
549 Note that you can not use this function for the peer relation, take a look
550 at expected_peer_units() for that.
551
552 This function will raise KeyError if you request information for a
553 relation type for which juju goal-state does not have information. It will
554 raise NotImplementedError if used with juju versions without goal-state
555 support.
556
557 Example usage:
558 log('participant {} of {} joined relation {}'
559 .format(len(related_units()),
560 len(list(expected_related_units())),
561 relation_type()))
562
563 :param reltype: Relation type to list data for, default is to list data for
564 the realtion type we are currently executing a hook for.
565 :type reltype: str
566 :returns: iterator
567 :rtype: types.GeneratorType
568 :raises: KeyError, NotImplementedError
569 """
570 if not has_juju_version("2.4.4"):
571 # goal-state existed in 2.4.0, but did not list individual units to
572 # join a relation in 2.4.1 through 2.4.3. (LP: #1794739)
573 raise NotImplementedError("goal-state relation unit count")
574 reltype = reltype or relation_type()
575 _goal_state = goal_state()
576 return (key for key in _goal_state['relations'][reltype] if '/' in key)
577
578
438@cached579@cached
439def relation_for_unit(unit=None, rid=None):580def relation_for_unit(unit=None, rid=None):
440 """Get the json represenation of a unit's relation"""581 """Get the json represenation of a unit's relation"""
@@ -478,6 +619,24 @@
478 return yaml.safe_load(md)619 return yaml.safe_load(md)
479620
480621
622def _metadata_unit(unit):
623 """Given the name of a unit (e.g. apache2/0), get the unit charm's
624 metadata.yaml. Very similar to metadata() but allows us to inspect
625 other units. Unit needs to be co-located, such as a subordinate or
626 principal/primary.
627
628 :returns: metadata.yaml as a python object.
629
630 """
631 basedir = os.sep.join(charm_dir().split(os.sep)[:-2])
632 unitdir = 'unit-{}'.format(unit.replace(os.sep, '-'))
633 joineddir = os.path.join(basedir, unitdir, 'charm', 'metadata.yaml')
634 if not os.path.exists(joineddir):
635 return None
636 with open(joineddir) as md:
637 return yaml.safe_load(md)
638
639
481@cached640@cached
482def relation_types():641def relation_types():
483 """Get a list of relation types supported by this charm"""642 """Get a list of relation types supported by this charm"""
@@ -602,20 +761,58 @@
602 return False761 return False
603762
604763
764def _port_op(op_name, port, protocol="TCP"):
765 """Open or close a service network port"""
766 _args = [op_name]
767 icmp = protocol.upper() == "ICMP"
768 if icmp:
769 _args.append(protocol)
770 else:
771 _args.append('{}/{}'.format(port, protocol))
772 try:
773 subprocess.check_call(_args)
774 except subprocess.CalledProcessError:
775 # Older Juju pre 2.3 doesn't support ICMP
776 # so treat it as a no-op if it fails.
777 if not icmp:
778 raise
779
780
605def open_port(port, protocol="TCP"):781def open_port(port, protocol="TCP"):
606 """Open a service network port"""782 """Open a service network port"""
607 _args = ['open-port']783 _port_op('open-port', port, protocol)
608 _args.append('{}/{}'.format(port, protocol))
609 subprocess.check_call(_args)
610784
611785
612def close_port(port, protocol="TCP"):786def close_port(port, protocol="TCP"):
613 """Close a service network port"""787 """Close a service network port"""
788 _port_op('close-port', port, protocol)
789
790
791def open_ports(start, end, protocol="TCP"):
792 """Opens a range of service network ports"""
793 _args = ['open-port']
794 _args.append('{}-{}/{}'.format(start, end, protocol))
795 subprocess.check_call(_args)
796
797
798def close_ports(start, end, protocol="TCP"):
799 """Close a range of service network ports"""
614 _args = ['close-port']800 _args = ['close-port']
615 _args.append('{}/{}'.format(port, protocol))801 _args.append('{}-{}/{}'.format(start, end, protocol))
616 subprocess.check_call(_args)802 subprocess.check_call(_args)
617803
618804
805def opened_ports():
806 """Get the opened ports
807
808 *Note that this will only show ports opened in a previous hook*
809
810 :returns: Opened ports as a list of strings: ``['8080/tcp', '8081-8083/tcp']``
811 """
812 _args = ['opened-ports', '--format=json']
813 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
814
815
619@cached816@cached
620def unit_get(attribute):817def unit_get(attribute):
621 """Get the unit ID for the remote unit"""818 """Get the unit ID for the remote unit"""
@@ -737,8 +934,15 @@
737 return wrapper934 return wrapper
738935
739936
937class NoNetworkBinding(Exception):
938 pass
939
940
740def charm_dir():941def charm_dir():
741 """Return the root directory of the current charm"""942 """Return the root directory of the current charm"""
943 d = os.environ.get('JUJU_CHARM_DIR')
944 if d is not None:
945 return d
742 return os.environ.get('CHARM_DIR')946 return os.environ.get('CHARM_DIR')
743947
744948
@@ -845,6 +1049,28 @@
845 return inner_translate_exc11049 return inner_translate_exc1
8461050
8471051
1052def application_version_set(version):
1053 """Charm authors may trigger this command from any hook to output what
1054 version of the application is running. This could be a package version,
1055 for instance postgres version 9.5. It could also be a build number or
1056 version control revision identifier, for instance git sha 6fb7ba68. """
1057
1058 cmd = ['application-version-set']
1059 cmd.append(version)
1060 try:
1061 subprocess.check_call(cmd)
1062 except OSError:
1063 log("Application Version: {}".format(version))
1064
1065
1066@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1067@cached
1068def goal_state():
1069 """Juju goal state values"""
1070 cmd = ['goal-state', '--format=json']
1071 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1072
1073
848@translate_exc(from_exc=OSError, to_exc=NotImplementedError)1074@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
849def is_leader():1075def is_leader():
850 """Does the current unit hold the juju leadership1076 """Does the current unit hold the juju leadership
@@ -912,6 +1138,24 @@
912 subprocess.check_call(cmd)1138 subprocess.check_call(cmd)
9131139
9141140
1141@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1142def resource_get(name):
1143 """used to fetch the resource path of the given name.
1144
1145 <name> must match a name of defined resource in metadata.yaml
1146
1147 returns either a path or False if resource not available
1148 """
1149 if not name:
1150 return False
1151
1152 cmd = ['resource-get', name]
1153 try:
1154 return subprocess.check_output(cmd).decode('UTF-8')
1155 except subprocess.CalledProcessError:
1156 return False
1157
1158
915@cached1159@cached
916def juju_version():1160def juju_version():
917 """Full version string (eg. '1.23.3.1-trusty-amd64')"""1161 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
@@ -921,7 +1165,6 @@
921 universal_newlines=True).strip()1165 universal_newlines=True).strip()
9221166
9231167
924@cached
925def has_juju_version(minimum_version):1168def has_juju_version(minimum_version):
926 """Return True if the Juju version is at least the provided version"""1169 """Return True if the Juju version is at least the provided version"""
927 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)1170 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
@@ -976,3 +1219,272 @@
976 for callback, args, kwargs in reversed(_atexit):1219 for callback, args, kwargs in reversed(_atexit):
977 callback(*args, **kwargs)1220 callback(*args, **kwargs)
978 del _atexit[:]1221 del _atexit[:]
1222
1223
1224@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1225def network_get_primary_address(binding):
1226 '''
1227 Deprecated since Juju 2.3; use network_get()
1228
1229 Retrieve the primary network address for a named binding
1230
1231 :param binding: string. The name of a relation of extra-binding
1232 :return: string. The primary IP address for the named binding
1233 :raise: NotImplementedError if run on Juju < 2.0
1234 '''
1235 cmd = ['network-get', '--primary-address', binding]
1236 try:
1237 response = subprocess.check_output(
1238 cmd,
1239 stderr=subprocess.STDOUT).decode('UTF-8').strip()
1240 except CalledProcessError as e:
1241 if 'no network config found for binding' in e.output.decode('UTF-8'):
1242 raise NoNetworkBinding("No network binding for {}"
1243 .format(binding))
1244 else:
1245 raise
1246 return response
1247
1248
1249def network_get(endpoint, relation_id=None):
1250 """
1251 Retrieve the network details for a relation endpoint
1252
1253 :param endpoint: string. The name of a relation endpoint
1254 :param relation_id: int. The ID of the relation for the current context.
1255 :return: dict. The loaded YAML output of the network-get query.
1256 :raise: NotImplementedError if request not supported by the Juju version.
1257 """
1258 if not has_juju_version('2.2'):
1259 raise NotImplementedError(juju_version()) # earlier versions require --primary-address
1260 if relation_id and not has_juju_version('2.3'):
1261 raise NotImplementedError # 2.3 added the -r option
1262
1263 cmd = ['network-get', endpoint, '--format', 'yaml']
1264 if relation_id:
1265 cmd.append('-r')
1266 cmd.append(relation_id)
1267 response = subprocess.check_output(
1268 cmd,
1269 stderr=subprocess.STDOUT).decode('UTF-8').strip()
1270 return yaml.safe_load(response)
1271
1272
1273def add_metric(*args, **kwargs):
1274 """Add metric values. Values may be expressed with keyword arguments. For
1275 metric names containing dashes, these may be expressed as one or more
1276 'key=value' positional arguments. May only be called from the collect-metrics
1277 hook."""
1278 _args = ['add-metric']
1279 _kvpairs = []
1280 _kvpairs.extend(args)
1281 _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1282 _args.extend(sorted(_kvpairs))
1283 try:
1284 subprocess.check_call(_args)
1285 return
1286 except EnvironmentError as e:
1287 if e.errno != errno.ENOENT:
1288 raise
1289 log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1290 log(log_message, level='INFO')
1291
1292
1293def meter_status():
1294 """Get the meter status, if running in the meter-status-changed hook."""
1295 return os.environ.get('JUJU_METER_STATUS')
1296
1297
1298def meter_info():
1299 """Get the meter status information, if running in the meter-status-changed
1300 hook."""
1301 return os.environ.get('JUJU_METER_INFO')
1302
1303
1304def iter_units_for_relation_name(relation_name):
1305 """Iterate through all units in a relation
1306
1307 Generator that iterates through all the units in a relation and yields
1308 a named tuple with rid and unit field names.
1309
1310 Usage:
1311 data = [(u.rid, u.unit)
1312 for u in iter_units_for_relation_name(relation_name)]
1313
1314 :param relation_name: string relation name
1315 :yield: Named Tuple with rid and unit field names
1316 """
1317 RelatedUnit = namedtuple('RelatedUnit', 'rid, unit')
1318 for rid in relation_ids(relation_name):
1319 for unit in related_units(rid):
1320 yield RelatedUnit(rid, unit)
1321
1322
1323def ingress_address(rid=None, unit=None):
1324 """
1325 Retrieve the ingress-address from a relation when available.
1326 Otherwise, return the private-address.
1327
1328 When used on the consuming side of the relation (unit is a remote
1329 unit), the ingress-address is the IP address that this unit needs
1330 to use to reach the provided service on the remote unit.
1331
1332 When used on the providing side of the relation (unit == local_unit()),
1333 the ingress-address is the IP address that is advertised to remote
1334 units on this relation. Remote units need to use this address to
1335 reach the local provided service on this unit.
1336
1337 Note that charms may document some other method to use in
1338 preference to the ingress_address(), such as an address provided
1339 on a different relation attribute or a service discovery mechanism.
1340 This allows charms to redirect inbound connections to their peers
1341 or different applications such as load balancers.
1342
1343 Usage:
1344 addresses = [ingress_address(rid=u.rid, unit=u.unit)
1345 for u in iter_units_for_relation_name(relation_name)]
1346
1347 :param rid: string relation id
1348 :param unit: string unit name
1349 :side effect: calls relation_get
1350 :return: string IP address
1351 """
1352 settings = relation_get(rid=rid, unit=unit)
1353 return (settings.get('ingress-address') or
1354 settings.get('private-address'))
1355
1356
1357def egress_subnets(rid=None, unit=None):
1358 """
1359 Retrieve the egress-subnets from a relation.
1360
1361 This function is to be used on the providing side of the
1362 relation, and provides the ranges of addresses that client
1363 connections may come from. The result is uninteresting on
1364 the consuming side of a relation (unit == local_unit()).
1365
1366 Returns a stable list of subnets in CIDR format.
1367 eg. ['192.168.1.0/24', '2001::F00F/128']
1368
1369 If egress-subnets is not available, falls back to using the published
1370 ingress-address, or finally private-address.
1371
1372 :param rid: string relation id
1373 :param unit: string unit name
1374 :side effect: calls relation_get
1375 :return: list of subnets in CIDR format. eg. ['192.168.1.0/24', '2001::F00F/128']
1376 """
1377 def _to_range(addr):
1378 if re.search(r'^(?:\d{1,3}\.){3}\d{1,3}$', addr) is not None:
1379 addr += '/32'
1380 elif ':' in addr and '/' not in addr: # IPv6
1381 addr += '/128'
1382 return addr
1383
1384 settings = relation_get(rid=rid, unit=unit)
1385 if 'egress-subnets' in settings:
1386 return [n.strip() for n in settings['egress-subnets'].split(',') if n.strip()]
1387 if 'ingress-address' in settings:
1388 return [_to_range(settings['ingress-address'])]
1389 if 'private-address' in settings:
1390 return [_to_range(settings['private-address'])]
1391 return [] # Should never happen
1392
1393
1394def unit_doomed(unit=None):
1395 """Determines if the unit is being removed from the model
1396
1397 Requires Juju 2.4.1.
1398
1399 :param unit: string unit name, defaults to local_unit
1400 :side effect: calls goal_state
1401 :side effect: calls local_unit
1402 :side effect: calls has_juju_version
1403 :return: True if the unit is being removed, already gone, or never existed
1404 """
1405 if not has_juju_version("2.4.1"):
1406 # We cannot risk blindly returning False for 'we don't know',
1407 # because that could cause data loss; if call sites don't
1408 # need an accurate answer, they likely don't need this helper
1409 # at all.
1410 # goal-state existed in 2.4.0, but did not handle removals
1411 # correctly until 2.4.1.
1412 raise NotImplementedError("is_doomed")
1413 if unit is None:
1414 unit = local_unit()
1415 gs = goal_state()
1416 units = gs.get('units', {})
1417 if unit not in units:
1418 return True
1419 # I don't think 'dead' units ever show up in the goal-state, but
1420 # check anyway in addition to 'dying'.
1421 return units[unit]['status'] in ('dying', 'dead')
1422
1423
1424def env_proxy_settings(selected_settings=None):
1425 """Get proxy settings from process environment variables.
1426
1427 Get charm proxy settings from environment variables that correspond to
1428 juju-http-proxy, juju-https-proxy and juju-no-proxy (available as of 2.4.2,
1429 see lp:1782236) in a format suitable for passing to an application that
1430 reacts to proxy settings passed as environment variables. Some applications
1431 support lowercase or uppercase notation (e.g. curl), some support only
1432 lowercase (e.g. wget), there are also subjectively rare cases of only
1433 uppercase notation support. no_proxy CIDR and wildcard support also varies
1434 between runtimes and applications as there is no enforced standard.
1435
1436 Some applications may connect to multiple destinations and expose config
1437 options that would affect only proxy settings for a specific destination
1438 these should be handled in charms in an application-specific manner.
1439
1440 :param selected_settings: format only a subset of possible settings
1441 :type selected_settings: list
1442 :rtype: Option(None, dict[str, str])
1443 """
1444 SUPPORTED_SETTINGS = {
1445 'http': 'HTTP_PROXY',
1446 'https': 'HTTPS_PROXY',
1447 'no_proxy': 'NO_PROXY',
1448 'ftp': 'FTP_PROXY'
1449 }
1450 if selected_settings is None:
1451 selected_settings = SUPPORTED_SETTINGS
1452
1453 selected_vars = [v for k, v in SUPPORTED_SETTINGS.items()
1454 if k in selected_settings]
1455 proxy_settings = {}
1456 for var in selected_vars:
1457 var_val = os.getenv(var)
1458 if var_val:
1459 proxy_settings[var] = var_val
1460 proxy_settings[var.lower()] = var_val
1461 # Now handle juju-prefixed environment variables. The legacy vs new
1462 # environment variable usage is mutually exclusive
1463 charm_var_val = os.getenv('JUJU_CHARM_{}'.format(var))
1464 if charm_var_val:
1465 proxy_settings[var] = charm_var_val
1466 proxy_settings[var.lower()] = charm_var_val
1467 if 'no_proxy' in proxy_settings:
1468 if _contains_range(proxy_settings['no_proxy']):
1469 log(RANGE_WARNING, level=WARNING)
1470 return proxy_settings if proxy_settings else None
1471
1472
1473def _contains_range(addresses):
1474 """Check for cidr or wildcard domain in a string.
1475
1476 Given a string comprising a comma seperated list of ip addresses
1477 and domain names, determine whether the string contains IP ranges
1478 or wildcard domains.
1479
1480 :param addresses: comma seperated list of domains and ip addresses.
1481 :type addresses: str
1482 """
1483 return (
1484 # Test for cidr (e.g. 10.20.20.0/24)
1485 "/" in addresses or
1486 # Test for wildcard domains (*.foo.com or .foo.com)
1487 "*" in addresses or
1488 addresses.startswith(".") or
1489 ",." in addresses or
1490 " ." in addresses)
9791491
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/host.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17"""Tools for working with the host system"""15"""Tools for working with the host system"""
18# Copyright 2012 Canonical Ltd.16# Copyright 2012 Canonical Ltd.
@@ -30,49 +28,175 @@
30import string28import string
31import subprocess29import subprocess
32import hashlib30import hashlib
31import functools
32import itertools
33import six
34
33from contextlib import contextmanager35from contextlib import contextmanager
34from collections import OrderedDict36from collections import OrderedDict
3537from .hookenv import log, INFO, DEBUG, local_unit, charm_name
36import six
37
38from .hookenv import log
39from .fstab import Fstab38from .fstab import Fstab
4039from charmhelpers.osplatform import get_platform
4140
42def service_start(service_name):41__platform__ = get_platform()
43 """Start a system service"""42if __platform__ == "ubuntu":
44 return service('start', service_name)43 from charmhelpers.core.host_factory.ubuntu import ( # NOQA:F401
4544 service_available,
4645 add_new_group,
47def service_stop(service_name):46 lsb_release,
48 """Stop a system service"""47 cmp_pkgrevno,
49 return service('stop', service_name)48 CompareHostReleases,
5049 get_distrib_codename,
5150 arch
52def service_restart(service_name):51 ) # flake8: noqa -- ignore F401 for this import
53 """Restart a system service"""52elif __platform__ == "centos":
53 from charmhelpers.core.host_factory.centos import ( # NOQA:F401
54 service_available,
55 add_new_group,
56 lsb_release,
57 cmp_pkgrevno,
58 CompareHostReleases,
59 ) # flake8: noqa -- ignore F401 for this import
60
61UPDATEDB_PATH = '/etc/updatedb.conf'
62
63
64def service_start(service_name, **kwargs):
65 """Start a system service.
66
67 The specified service name is managed via the system level init system.
68 Some init systems (e.g. upstart) require that additional arguments be
69 provided in order to directly control service instances whereas other init
70 systems allow for addressing instances of a service directly by name (e.g.
71 systemd).
72
73 The kwargs allow for the additional parameters to be passed to underlying
74 init systems for those systems which require/allow for them. For example,
75 the ceph-osd upstart script requires the id parameter to be passed along
76 in order to identify which running daemon should be reloaded. The follow-
77 ing example stops the ceph-osd service for instance id=4:
78
79 service_stop('ceph-osd', id=4)
80
81 :param service_name: the name of the service to stop
82 :param **kwargs: additional parameters to pass to the init system when
83 managing services. These will be passed as key=value
84 parameters to the init system's commandline. kwargs
85 are ignored for systemd enabled systems.
86 """
87 return service('start', service_name, **kwargs)
88
89
90def service_stop(service_name, **kwargs):
91 """Stop a system service.
92
93 The specified service name is managed via the system level init system.
94 Some init systems (e.g. upstart) require that additional arguments be
95 provided in order to directly control service instances whereas other init
96 systems allow for addressing instances of a service directly by name (e.g.
97 systemd).
98
99 The kwargs allow for the additional parameters to be passed to underlying
100 init systems for those systems which require/allow for them. For example,
101 the ceph-osd upstart script requires the id parameter to be passed along
102 in order to identify which running daemon should be reloaded. The follow-
103 ing example stops the ceph-osd service for instance id=4:
104
105 service_stop('ceph-osd', id=4)
106
107 :param service_name: the name of the service to stop
108 :param **kwargs: additional parameters to pass to the init system when
109 managing services. These will be passed as key=value
110 parameters to the init system's commandline. kwargs
111 are ignored for systemd enabled systems.
112 """
113 return service('stop', service_name, **kwargs)
114
115
116def service_restart(service_name, **kwargs):
117 """Restart a system service.
118
119 The specified service name is managed via the system level init system.
120 Some init systems (e.g. upstart) require that additional arguments be
121 provided in order to directly control service instances whereas other init
122 systems allow for addressing instances of a service directly by name (e.g.
123 systemd).
124
125 The kwargs allow for the additional parameters to be passed to underlying
126 init systems for those systems which require/allow for them. For example,
127 the ceph-osd upstart script requires the id parameter to be passed along
128 in order to identify which running daemon should be restarted. The follow-
129 ing example restarts the ceph-osd service for instance id=4:
130
131 service_restart('ceph-osd', id=4)
132
133 :param service_name: the name of the service to restart
134 :param **kwargs: additional parameters to pass to the init system when
135 managing services. These will be passed as key=value
136 parameters to the init system's commandline. kwargs
137 are ignored for init systems not allowing additional
138 parameters via the commandline (systemd).
139 """
54 return service('restart', service_name)140 return service('restart', service_name)
55141
56142
57def service_reload(service_name, restart_on_failure=False):143def service_reload(service_name, restart_on_failure=False, **kwargs):
58 """Reload a system service, optionally falling back to restart if144 """Reload a system service, optionally falling back to restart if
59 reload fails"""145 reload fails.
60 service_result = service('reload', service_name)146
147 The specified service name is managed via the system level init system.
148 Some init systems (e.g. upstart) require that additional arguments be
149 provided in order to directly control service instances whereas other init
150 systems allow for addressing instances of a service directly by name (e.g.
151 systemd).
152
153 The kwargs allow for the additional parameters to be passed to underlying
154 init systems for those systems which require/allow for them. For example,
155 the ceph-osd upstart script requires the id parameter to be passed along
156 in order to identify which running daemon should be reloaded. The follow-
157 ing example restarts the ceph-osd service for instance id=4:
158
159 service_reload('ceph-osd', id=4)
160
161 :param service_name: the name of the service to reload
162 :param restart_on_failure: boolean indicating whether to fallback to a
163 restart if the reload fails.
164 :param **kwargs: additional parameters to pass to the init system when
165 managing services. These will be passed as key=value
166 parameters to the init system's commandline. kwargs
167 are ignored for init systems not allowing additional
168 parameters via the commandline (systemd).
169 """
170 service_result = service('reload', service_name, **kwargs)
61 if not service_result and restart_on_failure:171 if not service_result and restart_on_failure:
62 service_result = service('restart', service_name)172 service_result = service('restart', service_name, **kwargs)
63 return service_result173 return service_result
64174
65175
66def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):176def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
177 **kwargs):
67 """Pause a system service.178 """Pause a system service.
68179
69 Stop it, and prevent it from starting again at boot."""180 Stop it, and prevent it from starting again at boot.
181
182 :param service_name: the name of the service to pause
183 :param init_dir: path to the upstart init directory
184 :param initd_dir: path to the sysv init directory
185 :param **kwargs: additional parameters to pass to the init system when
186 managing services. These will be passed as key=value
187 parameters to the init system's commandline. kwargs
188 are ignored for init systems which do not support
189 key=value arguments via the commandline.
190 """
70 stopped = True191 stopped = True
71 if service_running(service_name):192 if service_running(service_name, **kwargs):
72 stopped = service_stop(service_name)193 stopped = service_stop(service_name, **kwargs)
73 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))194 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
74 sysv_file = os.path.join(initd_dir, service_name)195 sysv_file = os.path.join(initd_dir, service_name)
75 if os.path.exists(upstart_file):196 if init_is_systemd():
197 service('disable', service_name)
198 service('mask', service_name)
199 elif os.path.exists(upstart_file):
76 override_path = os.path.join(200 override_path = os.path.join(
77 init_dir, '{}.override'.format(service_name))201 init_dir, '{}.override'.format(service_name))
78 with open(override_path, 'w') as fh:202 with open(override_path, 'w') as fh:
@@ -80,21 +204,33 @@
80 elif os.path.exists(sysv_file):204 elif os.path.exists(sysv_file):
81 subprocess.check_call(["update-rc.d", service_name, "disable"])205 subprocess.check_call(["update-rc.d", service_name, "disable"])
82 else:206 else:
83 # XXX: Support SystemD too
84 raise ValueError(207 raise ValueError(
85 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(208 "Unable to detect {0} as SystemD, Upstart {1} or"
209 " SysV {2}".format(
86 service_name, upstart_file, sysv_file))210 service_name, upstart_file, sysv_file))
87 return stopped211 return stopped
88212
89213
90def service_resume(service_name, init_dir="/etc/init",214def service_resume(service_name, init_dir="/etc/init",
91 initd_dir="/etc/init.d"):215 initd_dir="/etc/init.d", **kwargs):
92 """Resume a system service.216 """Resume a system service.
93217
94 Reenable starting again at boot. Start the service"""218 Reenable starting again at boot. Start the service.
219
220 :param service_name: the name of the service to resume
221 :param init_dir: the path to the init dir
222 :param initd dir: the path to the initd dir
223 :param **kwargs: additional parameters to pass to the init system when
224 managing services. These will be passed as key=value
225 parameters to the init system's commandline. kwargs
226 are ignored for systemd enabled systems.
227 """
95 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))228 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
96 sysv_file = os.path.join(initd_dir, service_name)229 sysv_file = os.path.join(initd_dir, service_name)
97 if os.path.exists(upstart_file):230 if init_is_systemd():
231 service('unmask', service_name)
232 service('enable', service_name)
233 elif os.path.exists(upstart_file):
98 override_path = os.path.join(234 override_path = os.path.join(
99 init_dir, '{}.override'.format(service_name))235 init_dir, '{}.override'.format(service_name))
100 if os.path.exists(override_path):236 if os.path.exists(override_path):
@@ -102,54 +238,90 @@
102 elif os.path.exists(sysv_file):238 elif os.path.exists(sysv_file):
103 subprocess.check_call(["update-rc.d", service_name, "enable"])239 subprocess.check_call(["update-rc.d", service_name, "enable"])
104 else:240 else:
105 # XXX: Support SystemD too
106 raise ValueError(241 raise ValueError(
107 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(242 "Unable to detect {0} as SystemD, Upstart {1} or"
243 " SysV {2}".format(
108 service_name, upstart_file, sysv_file))244 service_name, upstart_file, sysv_file))
245 started = service_running(service_name, **kwargs)
109246
110 started = service_running(service_name)
111 if not started:247 if not started:
112 started = service_start(service_name)248 started = service_start(service_name, **kwargs)
113 return started249 return started
114250
115251
116def service(action, service_name):252def service(action, service_name, **kwargs):
117 """Control a system service"""253 """Control a system service.
118 cmd = ['service', service_name, action]254
255 :param action: the action to take on the service
256 :param service_name: the name of the service to perform th action on
257 :param **kwargs: additional params to be passed to the service command in
258 the form of key=value.
259 """
260 if init_is_systemd():
261 cmd = ['systemctl', action, service_name]
262 else:
263 cmd = ['service', service_name, action]
264 for key, value in six.iteritems(kwargs):
265 parameter = '%s=%s' % (key, value)
266 cmd.append(parameter)
119 return subprocess.call(cmd) == 0267 return subprocess.call(cmd) == 0
120268
121269
122def service_running(service):270_UPSTART_CONF = "/etc/init/{}.conf"
123 """Determine whether a system service is running"""271_INIT_D_CONF = "/etc/init.d/{}"
124 try:272
125 output = subprocess.check_output(273
126 ['service', service, 'status'],274def service_running(service_name, **kwargs):
127 stderr=subprocess.STDOUT).decode('UTF-8')275 """Determine whether a system service is running.
128 except subprocess.CalledProcessError:276
129 return False277 :param service_name: the name of the service
130 else:278 :param **kwargs: additional args to pass to the service command. This is
131 if ("start/running" in output or "is running" in output):279 used to pass additional key=value arguments to the
132 return True280 service command line for managing specific instance
133 else:281 units (e.g. service ceph-osd status id=2). The kwargs
134 return False282 are ignored in systemd services.
135
136
137def service_available(service_name):
138 """Determine whether a system service is available"""
139 try:
140 subprocess.check_output(
141 ['service', service_name, 'status'],
142 stderr=subprocess.STDOUT).decode('UTF-8')
143 except subprocess.CalledProcessError as e:
144 return b'unrecognized service' not in e.output
145 else:
146 return True
147
148
149def adduser(username, password=None, shell='/bin/bash', system_user=False,
150 primary_group=None, secondary_groups=None):
151 """283 """
152 Add a user to the system.284 if init_is_systemd():
285 return service('is-active', service_name)
286 else:
287 if os.path.exists(_UPSTART_CONF.format(service_name)):
288 try:
289 cmd = ['status', service_name]
290 for key, value in six.iteritems(kwargs):
291 parameter = '%s=%s' % (key, value)
292 cmd.append(parameter)
293 output = subprocess.check_output(
294 cmd, stderr=subprocess.STDOUT).decode('UTF-8')
295 except subprocess.CalledProcessError:
296 return False
297 else:
298 # This works for upstart scripts where the 'service' command
299 # returns a consistent string to represent running
300 # 'start/running'
301 if ("start/running" in output or
302 "is running" in output or
303 "up and running" in output):
304 return True
305 elif os.path.exists(_INIT_D_CONF.format(service_name)):
306 # Check System V scripts init script return codes
307 return service('status', service_name)
308 return False
309
310
311SYSTEMD_SYSTEM = '/run/systemd/system'
312
313
314def init_is_systemd():
315 """Return True if the host system uses systemd, False otherwise."""
316 if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
317 return False
318 return os.path.isdir(SYSTEMD_SYSTEM)
319
320
321def adduser(username, password=None, shell='/bin/bash',
322 system_user=False, primary_group=None,
323 secondary_groups=None, uid=None, home_dir=None):
324 """Add a user to the system.
153325
154 Will log but otherwise succeed if the user already exists.326 Will log but otherwise succeed if the user already exists.
155327
@@ -157,17 +329,26 @@
157 :param str password: Password for user; if ``None``, create a system user329 :param str password: Password for user; if ``None``, create a system user
158 :param str shell: The default shell for the user330 :param str shell: The default shell for the user
159 :param bool system_user: Whether to create a login or system user331 :param bool system_user: Whether to create a login or system user
160 :param str primary_group: Primary group for user; defaults to their username332 :param str primary_group: Primary group for user; defaults to username
161 :param list secondary_groups: Optional list of additional groups333 :param list secondary_groups: Optional list of additional groups
334 :param int uid: UID for user being created
335 :param str home_dir: Home directory for user
162336
163 :returns: The password database entry struct, as returned by `pwd.getpwnam`337 :returns: The password database entry struct, as returned by `pwd.getpwnam`
164 """338 """
165 try:339 try:
166 user_info = pwd.getpwnam(username)340 user_info = pwd.getpwnam(username)
167 log('user {0} already exists!'.format(username))341 log('user {0} already exists!'.format(username))
342 if uid:
343 user_info = pwd.getpwuid(int(uid))
344 log('user with uid {0} already exists!'.format(uid))
168 except KeyError:345 except KeyError:
169 log('creating user {0}'.format(username))346 log('creating user {0}'.format(username))
170 cmd = ['useradd']347 cmd = ['useradd']
348 if uid:
349 cmd.extend(['--uid', str(uid)])
350 if home_dir:
351 cmd.extend(['--home', str(home_dir)])
171 if system_user or password is None:352 if system_user or password is None:
172 cmd.append('--system')353 cmd.append('--system')
173 else:354 else:
@@ -202,22 +383,56 @@
202 return user_exists383 return user_exists
203384
204385
205def add_group(group_name, system_group=False):386def uid_exists(uid):
206 """Add a group to the system"""387 """Check if a uid exists"""
388 try:
389 pwd.getpwuid(uid)
390 uid_exists = True
391 except KeyError:
392 uid_exists = False
393 return uid_exists
394
395
396def group_exists(groupname):
397 """Check if a group exists"""
398 try:
399 grp.getgrnam(groupname)
400 group_exists = True
401 except KeyError:
402 group_exists = False
403 return group_exists
404
405
406def gid_exists(gid):
407 """Check if a gid exists"""
408 try:
409 grp.getgrgid(gid)
410 gid_exists = True
411 except KeyError:
412 gid_exists = False
413 return gid_exists
414
415
416def add_group(group_name, system_group=False, gid=None):
417 """Add a group to the system
418
419 Will log but otherwise succeed if the group already exists.
420
421 :param str group_name: group to create
422 :param bool system_group: Create system group
423 :param int gid: GID for user being created
424
425 :returns: The password database entry struct, as returned by `grp.getgrnam`
426 """
207 try:427 try:
208 group_info = grp.getgrnam(group_name)428 group_info = grp.getgrnam(group_name)
209 log('group {0} already exists!'.format(group_name))429 log('group {0} already exists!'.format(group_name))
430 if gid:
431 group_info = grp.getgrgid(gid)
432 log('group with gid {0} already exists!'.format(gid))
210 except KeyError:433 except KeyError:
211 log('creating group {0}'.format(group_name))434 log('creating group {0}'.format(group_name))
212 cmd = ['addgroup']435 add_new_group(group_name, system_group, gid)
213 if system_group:
214 cmd.append('--system')
215 else:
216 cmd.extend([
217 '--group',
218 ])
219 cmd.append(group_name)
220 subprocess.check_call(cmd)
221 group_info = grp.getgrnam(group_name)436 group_info = grp.getgrnam(group_name)
222 return group_info437 return group_info
223438
@@ -229,15 +444,62 @@
229 subprocess.check_call(cmd)444 subprocess.check_call(cmd)
230445
231446
232def rsync(from_path, to_path, flags='-r', options=None):447def chage(username, lastday=None, expiredate=None, inactive=None,
448 mindays=None, maxdays=None, root=None, warndays=None):
449 """Change user password expiry information
450
451 :param str username: User to update
452 :param str lastday: Set when password was changed in YYYY-MM-DD format
453 :param str expiredate: Set when user's account will no longer be
454 accessible in YYYY-MM-DD format.
455 -1 will remove an account expiration date.
456 :param str inactive: Set the number of days of inactivity after a password
457 has expired before the account is locked.
458 -1 will remove an account's inactivity.
459 :param str mindays: Set the minimum number of days between password
460 changes to MIN_DAYS.
461 0 indicates the password can be changed anytime.
462 :param str maxdays: Set the maximum number of days during which a
463 password is valid.
464 -1 as MAX_DAYS will remove checking maxdays
465 :param str root: Apply changes in the CHROOT_DIR directory
466 :param str warndays: Set the number of days of warning before a password
467 change is required
468 :raises subprocess.CalledProcessError: if call to chage fails
469 """
470 cmd = ['chage']
471 if root:
472 cmd.extend(['--root', root])
473 if lastday:
474 cmd.extend(['--lastday', lastday])
475 if expiredate:
476 cmd.extend(['--expiredate', expiredate])
477 if inactive:
478 cmd.extend(['--inactive', inactive])
479 if mindays:
480 cmd.extend(['--mindays', mindays])
481 if maxdays:
482 cmd.extend(['--maxdays', maxdays])
483 if warndays:
484 cmd.extend(['--warndays', warndays])
485 cmd.append(username)
486 subprocess.check_call(cmd)
487
488
489remove_password_expiry = functools.partial(chage, expiredate='-1', inactive='-1', mindays='0', maxdays='-1')
490
491
492def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
233 """Replicate the contents of a path"""493 """Replicate the contents of a path"""
234 options = options or ['--delete', '--executability']494 options = options or ['--delete', '--executability']
235 cmd = ['/usr/bin/rsync', flags]495 cmd = ['/usr/bin/rsync', flags]
496 if timeout:
497 cmd = ['timeout', str(timeout)] + cmd
236 cmd.extend(options)498 cmd.extend(options)
237 cmd.append(from_path)499 cmd.append(from_path)
238 cmd.append(to_path)500 cmd.append(to_path)
239 log(" ".join(cmd))501 log(" ".join(cmd))
240 return subprocess.check_output(cmd).decode('UTF-8').strip()502 return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
241503
242504
243def symlink(source, destination):505def symlink(source, destination):
@@ -273,24 +535,54 @@
273535
274def write_file(path, content, owner='root', group='root', perms=0o444):536def write_file(path, content, owner='root', group='root', perms=0o444):
275 """Create or overwrite a file with the contents of a byte string."""537 """Create or overwrite a file with the contents of a byte string."""
276 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
277 uid = pwd.getpwnam(owner).pw_uid538 uid = pwd.getpwnam(owner).pw_uid
278 gid = grp.getgrnam(group).gr_gid539 gid = grp.getgrnam(group).gr_gid
279 with open(path, 'wb') as target:540 # lets see if we can grab the file and compare the context, to avoid doing
280 os.fchown(target.fileno(), uid, gid)541 # a write.
281 os.fchmod(target.fileno(), perms)542 existing_content = None
282 target.write(content)543 existing_uid, existing_gid, existing_perms = None, None, None
544 try:
545 with open(path, 'rb') as target:
546 existing_content = target.read()
547 stat = os.stat(path)
548 existing_uid, existing_gid, existing_perms = (
549 stat.st_uid, stat.st_gid, stat.st_mode
550 )
551 except Exception:
552 pass
553 if content != existing_content:
554 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms),
555 level=DEBUG)
556 with open(path, 'wb') as target:
557 os.fchown(target.fileno(), uid, gid)
558 os.fchmod(target.fileno(), perms)
559 if six.PY3 and isinstance(content, six.string_types):
560 content = content.encode('UTF-8')
561 target.write(content)
562 return
563 # the contents were the same, but we might still need to change the
564 # ownership or permissions.
565 if existing_uid != uid:
566 log("Changing uid on already existing content: {} -> {}"
567 .format(existing_uid, uid), level=DEBUG)
568 os.chown(path, uid, -1)
569 if existing_gid != gid:
570 log("Changing gid on already existing content: {} -> {}"
571 .format(existing_gid, gid), level=DEBUG)
572 os.chown(path, -1, gid)
573 if existing_perms != perms:
574 log("Changing permissions on existing content: {} -> {}"
575 .format(existing_perms, perms), level=DEBUG)
576 os.chmod(path, perms)
283577
284578
285def fstab_remove(mp):579def fstab_remove(mp):
286 """Remove the given mountpoint entry from /etc/fstab580 """Remove the given mountpoint entry from /etc/fstab"""
287 """
288 return Fstab.remove_by_mountpoint(mp)581 return Fstab.remove_by_mountpoint(mp)
289582
290583
291def fstab_add(dev, mp, fs, options=None):584def fstab_add(dev, mp, fs, options=None):
292 """Adds the given device entry to the /etc/fstab file585 """Adds the given device entry to the /etc/fstab file"""
293 """
294 return Fstab.add(dev, mp, fs, options=options)586 return Fstab.add(dev, mp, fs, options=options)
295587
296588
@@ -346,8 +638,7 @@
346638
347639
348def file_hash(path, hash_type='md5'):640def file_hash(path, hash_type='md5'):
349 """641 """Generate a hash checksum of the contents of 'path' or None if not found.
350 Generate a hash checksum of the contents of 'path' or None if not found.
351642
352 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,643 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
353 such as md5, sha1, sha256, sha512, etc.644 such as md5, sha1, sha256, sha512, etc.
@@ -362,10 +653,9 @@
362653
363654
364def path_hash(path):655def path_hash(path):
365 """656 """Generate a hash checksum of all files matching 'path'. Standard
366 Generate a hash checksum of all files matching 'path'. Standard wildcards657 wildcards like '*' and '?' are supported, see documentation for the 'glob'
367 like '*' and '?' are supported, see documentation for the 'glob' module for658 module for more information.
368 more information.
369659
370 :return: dict: A { filename: hash } dictionary for all matched files.660 :return: dict: A { filename: hash } dictionary for all matched files.
371 Empty if none found.661 Empty if none found.
@@ -377,8 +667,7 @@
377667
378668
379def check_hash(path, checksum, hash_type='md5'):669def check_hash(path, checksum, hash_type='md5'):
380 """670 """Validate a file using a cryptographic checksum.
381 Validate a file using a cryptographic checksum.
382671
383 :param str checksum: Value of the checksum used to validate the file.672 :param str checksum: Value of the checksum used to validate the file.
384 :param str hash_type: Hash algorithm used to generate `checksum`.673 :param str hash_type: Hash algorithm used to generate `checksum`.
@@ -393,10 +682,11 @@
393682
394683
395class ChecksumError(ValueError):684class ChecksumError(ValueError):
685 """A class derived from Value error to indicate the checksum failed."""
396 pass686 pass
397687
398688
399def restart_on_change(restart_map, stopstart=False):689def restart_on_change(restart_map, stopstart=False, restart_functions=None):
400 """Restart services based on configuration files changing690 """Restart services based on configuration files changing
401691
402 This function is used a decorator, for example::692 This function is used a decorator, for example::
@@ -414,35 +704,56 @@
414 restarted if any file matching the pattern got changed, created704 restarted if any file matching the pattern got changed, created
415 or removed. Standard wildcards are supported, see documentation705 or removed. Standard wildcards are supported, see documentation
416 for the 'glob' module for more information.706 for the 'glob' module for more information.
707
708 @param restart_map: {path_file_name: [service_name, ...]
709 @param stopstart: DEFAULT false; whether to stop, start OR restart
710 @param restart_functions: nonstandard functions to use to restart services
711 {svc: func, ...}
712 @returns result from decorated function
417 """713 """
418 def wrap(f):714 def wrap(f):
715 @functools.wraps(f)
419 def wrapped_f(*args, **kwargs):716 def wrapped_f(*args, **kwargs):
420 checksums = {path: path_hash(path) for path in restart_map}717 return restart_on_change_helper(
421 f(*args, **kwargs)718 (lambda: f(*args, **kwargs)), restart_map, stopstart,
422 restarts = []719 restart_functions)
423 for path in restart_map:
424 if path_hash(path) != checksums[path]:
425 restarts += restart_map[path]
426 services_list = list(OrderedDict.fromkeys(restarts))
427 if not stopstart:
428 for service_name in services_list:
429 service('restart', service_name)
430 else:
431 for action in ['stop', 'start']:
432 for service_name in services_list:
433 service(action, service_name)
434 return wrapped_f720 return wrapped_f
435 return wrap721 return wrap
436722
437723
438def lsb_release():724def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
439 """Return /etc/lsb-release in a dict"""725 restart_functions=None):
440 d = {}726 """Helper function to perform the restart_on_change function.
441 with open('/etc/lsb-release', 'r') as lsb:727
442 for l in lsb:728 This is provided for decorators to restart services if files described
443 k, v = l.split('=')729 in the restart_map have changed after an invocation of lambda_f().
444 d[k.strip()] = v.strip()730
445 return d731 @param lambda_f: function to call.
732 @param restart_map: {file: [service, ...]}
733 @param stopstart: whether to stop, start or restart a service
734 @param restart_functions: nonstandard functions to use to restart services
735 {svc: func, ...}
736 @returns result of lambda_f()
737 """
738 if restart_functions is None:
739 restart_functions = {}
740 checksums = {path: path_hash(path) for path in restart_map}
741 r = lambda_f()
742 # create a list of lists of the services to restart
743 restarts = [restart_map[path]
744 for path in restart_map
745 if path_hash(path) != checksums[path]]
746 # create a flat list of ordered services without duplicates from lists
747 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
748 if services_list:
749 actions = ('stop', 'start') if stopstart else ('restart',)
750 for service_name in services_list:
751 if service_name in restart_functions:
752 restart_functions[service_name](service_name)
753 else:
754 for action in actions:
755 service(action, service_name)
756 return r
446757
447758
448def pwgen(length=None):759def pwgen(length=None):
@@ -498,7 +809,7 @@
498809
499810
500def list_nics(nic_type=None):811def list_nics(nic_type=None):
501 '''Return a list of nics of given type(s)'''812 """Return a list of nics of given type(s)"""
502 if isinstance(nic_type, six.string_types):813 if isinstance(nic_type, six.string_types):
503 int_types = [nic_type]814 int_types = [nic_type]
504 else:815 else:
@@ -527,7 +838,7 @@
527 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')838 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
528 ip_output = (line.strip() for line in ip_output if line)839 ip_output = (line.strip() for line in ip_output if line)
529840
530 key = re.compile('^[0-9]+:\s+(.+):')841 key = re.compile(r'^[0-9]+:\s+(.+):')
531 for line in ip_output:842 for line in ip_output:
532 matched = re.search(key, line)843 matched = re.search(key, line)
533 if matched:844 if matched:
@@ -540,12 +851,13 @@
540851
541852
542def set_nic_mtu(nic, mtu):853def set_nic_mtu(nic, mtu):
543 '''Set MTU on a network interface'''854 """Set the Maximum Transmission Unit (MTU) on a network interface."""
544 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]855 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
545 subprocess.check_call(cmd)856 subprocess.check_call(cmd)
546857
547858
548def get_nic_mtu(nic):859def get_nic_mtu(nic):
860 """Return the Maximum Transmission Unit (MTU) for a network interface."""
549 cmd = ['ip', 'addr', 'show', nic]861 cmd = ['ip', 'addr', 'show', nic]
550 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')862 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
551 mtu = ""863 mtu = ""
@@ -557,6 +869,7 @@
557869
558870
559def get_nic_hwaddr(nic):871def get_nic_hwaddr(nic):
872 """Return the Media Access Control (MAC) for a network interface."""
560 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]873 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
561 ip_output = subprocess.check_output(cmd).decode('UTF-8')874 ip_output = subprocess.check_output(cmd).decode('UTF-8')
562 hwaddr = ""875 hwaddr = ""
@@ -566,40 +879,29 @@
566 return hwaddr879 return hwaddr
567880
568881
569def cmp_pkgrevno(package, revno, pkgcache=None):
570 '''Compare supplied revno with the revno of the installed package
571
572 * 1 => Installed revno is greater than supplied arg
573 * 0 => Installed revno is the same as supplied arg
574 * -1 => Installed revno is less than supplied arg
575
576 This function imports apt_cache function from charmhelpers.fetch if
577 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
578 you call this function, or pass an apt_pkg.Cache() instance.
579 '''
580 import apt_pkg
581 if not pkgcache:
582 from charmhelpers.fetch import apt_cache
583 pkgcache = apt_cache()
584 pkg = pkgcache[package]
585 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
586
587
588@contextmanager882@contextmanager
589def chdir(d):883def chdir(directory):
884 """Change the current working directory to a different directory for a code
885 block and return the previous directory after the block exits. Useful to
886 run commands from a specificed directory.
887
888 :param str directory: The directory path to change to for this context.
889 """
590 cur = os.getcwd()890 cur = os.getcwd()
591 try:891 try:
592 yield os.chdir(d)892 yield os.chdir(directory)
593 finally:893 finally:
594 os.chdir(cur)894 os.chdir(cur)
595895
596896
597def chownr(path, owner, group, follow_links=True, chowntopdir=False):897def chownr(path, owner, group, follow_links=True, chowntopdir=False):
598 """898 """Recursively change user and group ownership of files and directories
599 Recursively change user and group ownership of files and directories
600 in given path. Doesn't chown path itself by default, only its children.899 in given path. Doesn't chown path itself by default, only its children.
601900
602 :param bool follow_links: Also Chown links if True901 :param str path: The string path to start changing ownership.
902 :param str owner: The owner string to use when looking up the uid.
903 :param str group: The group string to use when looking up the gid.
904 :param bool follow_links: Also follow and chown links if True
603 :param bool chowntopdir: Also chown path itself if True905 :param bool chowntopdir: Also chown path itself if True
604 """906 """
605 uid = pwd.getpwnam(owner).pw_uid907 uid = pwd.getpwnam(owner).pw_uid
@@ -613,7 +915,7 @@
613 broken_symlink = os.path.lexists(path) and not os.path.exists(path)915 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
614 if not broken_symlink:916 if not broken_symlink:
615 chown(path, uid, gid)917 chown(path, uid, gid)
616 for root, dirs, files in os.walk(path):918 for root, dirs, files in os.walk(path, followlinks=follow_links):
617 for name in dirs + files:919 for name in dirs + files:
618 full = os.path.join(root, name)920 full = os.path.join(root, name)
619 broken_symlink = os.path.lexists(full) and not os.path.exists(full)921 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
@@ -622,15 +924,37 @@
622924
623925
624def lchownr(path, owner, group):926def lchownr(path, owner, group):
927 """Recursively change user and group ownership of files and directories
928 in a given path, not following symbolic links. See the documentation for
929 'os.lchown' for more information.
930
931 :param str path: The string path to start changing ownership.
932 :param str owner: The owner string to use when looking up the uid.
933 :param str group: The group string to use when looking up the gid.
934 """
625 chownr(path, owner, group, follow_links=False)935 chownr(path, owner, group, follow_links=False)
626936
627937
938def owner(path):
939 """Returns a tuple containing the username & groupname owning the path.
940
941 :param str path: the string path to retrieve the ownership
942 :return tuple(str, str): A (username, groupname) tuple containing the
943 name of the user and group owning the path.
944 :raises OSError: if the specified path does not exist
945 """
946 stat = os.stat(path)
947 username = pwd.getpwuid(stat.st_uid)[0]
948 groupname = grp.getgrgid(stat.st_gid)[0]
949 return username, groupname
950
951
628def get_total_ram():952def get_total_ram():
629 '''The total amount of system RAM in bytes.953 """The total amount of system RAM in bytes.
630954
631 This is what is reported by the OS, and may be overcommitted when955 This is what is reported by the OS, and may be overcommitted when
632 there are multiple containers hosted on the same machine.956 there are multiple containers hosted on the same machine.
633 '''957 """
634 with open('/proc/meminfo', 'r') as f:958 with open('/proc/meminfo', 'r') as f:
635 for line in f.readlines():959 for line in f.readlines():
636 if line:960 if line:
@@ -639,3 +963,115 @@
639 assert unit == 'kB', 'Unknown unit'963 assert unit == 'kB', 'Unknown unit'
640 return int(value) * 1024 # Classic, not KiB.964 return int(value) * 1024 # Classic, not KiB.
641 raise NotImplementedError()965 raise NotImplementedError()
966
967
968UPSTART_CONTAINER_TYPE = '/run/container_type'
969
970
971def is_container():
972 """Determine whether unit is running in a container
973
974 @return: boolean indicating if unit is in a container
975 """
976 if init_is_systemd():
977 # Detect using systemd-detect-virt
978 return subprocess.call(['systemd-detect-virt',
979 '--container']) == 0
980 else:
981 # Detect using upstart container file marker
982 return os.path.exists(UPSTART_CONTAINER_TYPE)
983
984
985def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
986 """Adds the specified path to the mlocate's udpatedb.conf PRUNEPATH list.
987
988 This method has no effect if the path specified by updatedb_path does not
989 exist or is not a file.
990
991 @param path: string the path to add to the updatedb.conf PRUNEPATHS value
992 @param updatedb_path: the path the updatedb.conf file
993 """
994 if not os.path.exists(updatedb_path) or os.path.isdir(updatedb_path):
995 # If the updatedb.conf file doesn't exist then don't attempt to update
996 # the file as the package providing mlocate may not be installed on
997 # the local system
998 return
999
1000 with open(updatedb_path, 'r+') as f_id:
1001 updatedb_text = f_id.read()
1002 output = updatedb(updatedb_text, path)
1003 f_id.seek(0)
1004 f_id.write(output)
1005 f_id.truncate()
1006
1007
1008def updatedb(updatedb_text, new_path):
1009 lines = [line for line in updatedb_text.split("\n")]
1010 for i, line in enumerate(lines):
1011 if line.startswith("PRUNEPATHS="):
1012 paths_line = line.split("=")[1].replace('"', '')
1013 paths = paths_line.split(" ")
1014 if new_path not in paths:
1015 paths.append(new_path)
1016 lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
1017 output = "\n".join(lines)
1018 return output
1019
1020
1021def modulo_distribution(modulo=3, wait=30, non_zero_wait=False):
1022 """ Modulo distribution
1023
1024 This helper uses the unit number, a modulo value and a constant wait time
1025 to produce a calculated wait time distribution. This is useful in large
1026 scale deployments to distribute load during an expensive operation such as
1027 service restarts.
1028
1029 If you have 1000 nodes that need to restart 100 at a time 1 minute at a
1030 time:
1031
1032 time.wait(modulo_distribution(modulo=100, wait=60))
1033 restart()
1034
1035 If you need restarts to happen serially set modulo to the exact number of
1036 nodes and set a high constant wait time:
1037
1038 time.wait(modulo_distribution(modulo=10, wait=120))
1039 restart()
1040
1041 @param modulo: int The modulo number creates the group distribution
1042 @param wait: int The constant time wait value
1043 @param non_zero_wait: boolean Override unit % modulo == 0,
1044 return modulo * wait. Used to avoid collisions with
1045 leader nodes which are often given priority.
1046 @return: int Calculated time to wait for unit operation
1047 """
1048 unit_number = int(local_unit().split('/')[1])
1049 calculated_wait_time = (unit_number % modulo) * wait
1050 if non_zero_wait and calculated_wait_time == 0:
1051 return modulo * wait
1052 else:
1053 return calculated_wait_time
1054
1055
1056def install_ca_cert(ca_cert, name=None):
1057 """
1058 Install the given cert as a trusted CA.
1059
1060 The ``name`` is the stem of the filename where the cert is written, and if
1061 not provided, it will default to ``juju-{charm_name}``.
1062
1063 If the cert is empty or None, or is unchanged, nothing is done.
1064 """
1065 if not ca_cert:
1066 return
1067 if not isinstance(ca_cert, bytes):
1068 ca_cert = ca_cert.encode('utf8')
1069 if not name:
1070 name = 'juju-{}'.format(charm_name())
1071 cert_file = '/usr/local/share/ca-certificates/{}.crt'.format(name)
1072 new_hash = hashlib.md5(ca_cert).hexdigest()
1073 if file_hash(cert_file) == new_hash:
1074 return
1075 log("Installing new CA cert at: {}".format(cert_file), level=INFO)
1076 write_file(cert_file, ca_cert)
1077 subprocess.check_call(['update-ca-certificates', '--fresh'])
6421078
=== added directory 'charmhelpers/core/host_factory'
=== added file 'charmhelpers/core/host_factory/__init__.py'
=== added file 'charmhelpers/core/host_factory/centos.py'
--- charmhelpers/core/host_factory/centos.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/host_factory/centos.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,72 @@
1import subprocess
2import yum
3import os
4
5from charmhelpers.core.strutils import BasicStringComparator
6
7
8class CompareHostReleases(BasicStringComparator):
9 """Provide comparisons of Host releases.
10
11 Use in the form of
12
13 if CompareHostReleases(release) > 'trusty':
14 # do something with mitaka
15 """
16
17 def __init__(self, item):
18 raise NotImplementedError(
19 "CompareHostReleases() is not implemented for CentOS")
20
21
22def service_available(service_name):
23 # """Determine whether a system service is available."""
24 if os.path.isdir('/run/systemd/system'):
25 cmd = ['systemctl', 'is-enabled', service_name]
26 else:
27 cmd = ['service', service_name, 'is-enabled']
28 return subprocess.call(cmd) == 0
29
30
31def add_new_group(group_name, system_group=False, gid=None):
32 cmd = ['groupadd']
33 if gid:
34 cmd.extend(['--gid', str(gid)])
35 if system_group:
36 cmd.append('-r')
37 cmd.append(group_name)
38 subprocess.check_call(cmd)
39
40
41def lsb_release():
42 """Return /etc/os-release in a dict."""
43 d = {}
44 with open('/etc/os-release', 'r') as lsb:
45 for l in lsb:
46 s = l.split('=')
47 if len(s) != 2:
48 continue
49 d[s[0].strip()] = s[1].strip()
50 return d
51
52
53def cmp_pkgrevno(package, revno, pkgcache=None):
54 """Compare supplied revno with the revno of the installed package.
55
56 * 1 => Installed revno is greater than supplied arg
57 * 0 => Installed revno is the same as supplied arg
58 * -1 => Installed revno is less than supplied arg
59
60 This function imports YumBase function if the pkgcache argument
61 is None.
62 """
63 if not pkgcache:
64 y = yum.YumBase()
65 packages = y.doPackageLists()
66 pkgcache = {i.Name: i.version for i in packages['installed']}
67 pkg = pkgcache[package]
68 if pkg > revno:
69 return 1
70 if pkg < revno:
71 return -1
72 return 0
073
=== added file 'charmhelpers/core/host_factory/ubuntu.py'
--- charmhelpers/core/host_factory/ubuntu.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/host_factory/ubuntu.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,114 @@
1import subprocess
2
3from charmhelpers.core.hookenv import cached
4from charmhelpers.core.strutils import BasicStringComparator
5
6
7UBUNTU_RELEASES = (
8 'lucid',
9 'maverick',
10 'natty',
11 'oneiric',
12 'precise',
13 'quantal',
14 'raring',
15 'saucy',
16 'trusty',
17 'utopic',
18 'vivid',
19 'wily',
20 'xenial',
21 'yakkety',
22 'zesty',
23 'artful',
24 'bionic',
25 'cosmic',
26 'disco',
27)
28
29
30class CompareHostReleases(BasicStringComparator):
31 """Provide comparisons of Ubuntu releases.
32
33 Use in the form of
34
35 if CompareHostReleases(release) > 'trusty':
36 # do something with mitaka
37 """
38 _list = UBUNTU_RELEASES
39
40
41def service_available(service_name):
42 """Determine whether a system service is available"""
43 try:
44 subprocess.check_output(
45 ['service', service_name, 'status'],
46 stderr=subprocess.STDOUT).decode('UTF-8')
47 except subprocess.CalledProcessError as e:
48 return b'unrecognized service' not in e.output
49 else:
50 return True
51
52
53def add_new_group(group_name, system_group=False, gid=None):
54 cmd = ['addgroup']
55 if gid:
56 cmd.extend(['--gid', str(gid)])
57 if system_group:
58 cmd.append('--system')
59 else:
60 cmd.extend([
61 '--group',
62 ])
63 cmd.append(group_name)
64 subprocess.check_call(cmd)
65
66
67def lsb_release():
68 """Return /etc/lsb-release in a dict"""
69 d = {}
70 with open('/etc/lsb-release', 'r') as lsb:
71 for l in lsb:
72 k, v = l.split('=')
73 d[k.strip()] = v.strip()
74 return d
75
76
77def get_distrib_codename():
78 """Return the codename of the distribution
79 :returns: The codename
80 :rtype: str
81 """
82 return lsb_release()['DISTRIB_CODENAME'].lower()
83
84
85def cmp_pkgrevno(package, revno, pkgcache=None):
86 """Compare supplied revno with the revno of the installed package.
87
88 * 1 => Installed revno is greater than supplied arg
89 * 0 => Installed revno is the same as supplied arg
90 * -1 => Installed revno is less than supplied arg
91
92 This function imports apt_cache function from charmhelpers.fetch if
93 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
94 you call this function, or pass an apt_pkg.Cache() instance.
95 """
96 import apt_pkg
97 if not pkgcache:
98 from charmhelpers.fetch import apt_cache
99 pkgcache = apt_cache()
100 pkg = pkgcache[package]
101 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
102
103
104@cached
105def arch():
106 """Return the package architecture as a string.
107
108 :returns: the architecture
109 :rtype: str
110 :raises: subprocess.CalledProcessError if dpkg command fails
111 """
112 return subprocess.check_output(
113 ['dpkg', '--print-architecture']
114 ).rstrip().decode('UTF-8')
0115
=== modified file 'charmhelpers/core/hugepage.py'
--- charmhelpers/core/hugepage.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/hugepage.py 2021-04-13 19:16:05 +0000
@@ -2,19 +2,17 @@
22
3# Copyright 2014-2015 Canonical Limited.3# Copyright 2014-2015 Canonical Limited.
4#4#
5# This file is part of charm-helpers.5# Licensed under the Apache License, Version 2.0 (the "License");
6#6# you may not use this file except in compliance with the License.
7# charm-helpers is free software: you can redistribute it and/or modify7# You may obtain a copy of the License at
8# it under the terms of the GNU Lesser General Public License version 3 as8#
9# published by the Free Software Foundation.9# http://www.apache.org/licenses/LICENSE-2.0
10#10#
11# charm-helpers is distributed in the hope that it will be useful,11# Unless required by applicable law or agreed to in writing, software
12# but WITHOUT ANY WARRANTY; without even the implied warranty of12# distributed under the License is distributed on an "AS IS" BASIS,
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# GNU Lesser General Public License for more details.14# See the License for the specific language governing permissions and
15#15# limitations under the License.
16# You should have received a copy of the GNU Lesser General Public License
17# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1816
19import yaml17import yaml
20from charmhelpers.core import fstab18from charmhelpers.core import fstab
2119
=== modified file 'charmhelpers/core/kernel.py'
--- charmhelpers/core/kernel.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/kernel.py 2021-04-13 19:16:05 +0000
@@ -3,29 +3,40 @@
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License17
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.18import re
1919import subprocess
20__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"20
2121from charmhelpers.osplatform import get_platform
22from charmhelpers.core.hookenv import (22from charmhelpers.core.hookenv import (
23 log,23 log,
24 INFO24 INFO
25)25)
2626
27from subprocess import check_call, check_output27__platform__ = get_platform()
28import re28if __platform__ == "ubuntu":
29 from charmhelpers.core.kernel_factory.ubuntu import ( # NOQA:F401
30 persistent_modprobe,
31 update_initramfs,
32 ) # flake8: noqa -- ignore F401 for this import
33elif __platform__ == "centos":
34 from charmhelpers.core.kernel_factory.centos import ( # NOQA:F401
35 persistent_modprobe,
36 update_initramfs,
37 ) # flake8: noqa -- ignore F401 for this import
38
39__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
2940
3041
31def modprobe(module, persist=True):42def modprobe(module, persist=True):
@@ -34,11 +45,9 @@
3445
35 log('Loading kernel module %s' % module, level=INFO)46 log('Loading kernel module %s' % module, level=INFO)
3647
37 check_call(cmd)48 subprocess.check_call(cmd)
38 if persist:49 if persist:
39 with open('/etc/modules', 'r+') as modules:50 persistent_modprobe(module)
40 if module not in modules.read():
41 modules.write(module)
4251
4352
44def rmmod(module, force=False):53def rmmod(module, force=False):
@@ -48,21 +57,16 @@
48 cmd.append('-f')57 cmd.append('-f')
49 cmd.append(module)58 cmd.append(module)
50 log('Removing kernel module %s' % module, level=INFO)59 log('Removing kernel module %s' % module, level=INFO)
51 return check_call(cmd)60 return subprocess.check_call(cmd)
5261
5362
54def lsmod():63def lsmod():
55 """Shows what kernel modules are currently loaded"""64 """Shows what kernel modules are currently loaded"""
56 return check_output(['lsmod'],65 return subprocess.check_output(['lsmod'],
57 universal_newlines=True)66 universal_newlines=True)
5867
5968
60def is_module_loaded(module):69def is_module_loaded(module):
61 """Checks if a kernel module is already loaded"""70 """Checks if a kernel module is already loaded"""
62 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)71 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
63 return len(matches) > 072 return len(matches) > 0
64
65
66def update_initramfs(version='all'):
67 """Updates an initramfs image"""
68 return check_call(["update-initramfs", "-k", version, "-u"])
6973
=== added directory 'charmhelpers/core/kernel_factory'
=== added file 'charmhelpers/core/kernel_factory/__init__.py'
=== added file 'charmhelpers/core/kernel_factory/centos.py'
--- charmhelpers/core/kernel_factory/centos.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/kernel_factory/centos.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,17 @@
1import subprocess
2import os
3
4
5def persistent_modprobe(module):
6 """Load a kernel module and configure for auto-load on reboot."""
7 if not os.path.exists('/etc/rc.modules'):
8 open('/etc/rc.modules', 'a')
9 os.chmod('/etc/rc.modules', 111)
10 with open('/etc/rc.modules', 'r+') as modules:
11 if module not in modules.read():
12 modules.write('modprobe %s\n' % module)
13
14
15def update_initramfs(version='all'):
16 """Updates an initramfs image."""
17 return subprocess.check_call(["dracut", "-f", version])
018
=== added file 'charmhelpers/core/kernel_factory/ubuntu.py'
--- charmhelpers/core/kernel_factory/ubuntu.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/kernel_factory/ubuntu.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,13 @@
1import subprocess
2
3
4def persistent_modprobe(module):
5 """Load a kernel module and configure for auto-load on reboot."""
6 with open('/etc/modules', 'r+') as modules:
7 if module not in modules.read():
8 modules.write(module + "\n")
9
10
11def update_initramfs(version='all'):
12 """Updates an initramfs image."""
13 return subprocess.check_call(["update-initramfs", "-k", version, "-u"])
014
=== modified file 'charmhelpers/core/services/__init__.py'
--- charmhelpers/core/services/__init__.py 2015-01-28 08:59:02 +0000
+++ charmhelpers/core/services/__init__.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17from .base import * # NOQA15from .base import * # NOQA
18from .helpers import * # NOQA16from .helpers import * # NOQA
1917
=== modified file 'charmhelpers/core/services/base.py'
--- charmhelpers/core/services/base.py 2015-07-03 09:13:26 +0000
+++ charmhelpers/core/services/base.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
18import json16import json
@@ -309,23 +307,34 @@
309 """307 """
310 def __call__(self, manager, service_name, event_name):308 def __call__(self, manager, service_name, event_name):
311 service = manager.get_service(service_name)309 service = manager.get_service(service_name)
312 new_ports = service.get('ports', [])310 # turn this generator into a list,
311 # as we'll be going over it multiple times
312 new_ports = list(service.get('ports', []))
313 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))313 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
314 if os.path.exists(port_file):314 if os.path.exists(port_file):
315 with open(port_file) as fp:315 with open(port_file) as fp:
316 old_ports = fp.read().split(',')316 old_ports = fp.read().split(',')
317 for old_port in old_ports:317 for old_port in old_ports:
318 if bool(old_port):318 if bool(old_port) and not self.ports_contains(old_port, new_ports):
319 old_port = int(old_port)319 hookenv.close_port(old_port)
320 if old_port not in new_ports:
321 hookenv.close_port(old_port)
322 with open(port_file, 'w') as fp:320 with open(port_file, 'w') as fp:
323 fp.write(','.join(str(port) for port in new_ports))321 fp.write(','.join(str(port) for port in new_ports))
324 for port in new_ports:322 for port in new_ports:
323 # A port is either a number or 'ICMP'
324 protocol = 'TCP'
325 if str(port).upper() == 'ICMP':
326 protocol = 'ICMP'
325 if event_name == 'start':327 if event_name == 'start':
326 hookenv.open_port(port)328 hookenv.open_port(port, protocol)
327 elif event_name == 'stop':329 elif event_name == 'stop':
328 hookenv.close_port(port)330 hookenv.close_port(port, protocol)
331
332 def ports_contains(self, port, ports):
333 if not bool(port):
334 return False
335 if str(port).upper() != 'ICMP':
336 port = int(port)
337 return port in ports
329338
330339
331def service_stop(service_name):340def service_stop(service_name):
332341
=== modified file 'charmhelpers/core/services/helpers.py'
--- charmhelpers/core/services/helpers.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/services/helpers.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
18import yaml16import yaml
1917
=== modified file 'charmhelpers/core/strutils.py'
--- charmhelpers/core/strutils.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/strutils.py 2021-04-13 19:16:05 +0000
@@ -3,19 +3,17 @@
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1917
20import six18import six
21import re19import re
@@ -63,10 +61,69 @@
63 if isinstance(value, six.string_types):61 if isinstance(value, six.string_types):
64 value = six.text_type(value)62 value = six.text_type(value)
65 else:63 else:
66 msg = "Unable to interpret non-string value '%s' as boolean" % (value)64 msg = "Unable to interpret non-string value '%s' as bytes" % (value)
67 raise ValueError(msg)65 raise ValueError(msg)
68 matches = re.match("([0-9]+)([a-zA-Z]+)", value)66 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
69 if not matches:67 if matches:
70 msg = "Unable to interpret string value '%s' as bytes" % (value)68 size = int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
71 raise ValueError(msg)69 else:
72 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])70 # Assume that value passed in is bytes
71 try:
72 size = int(value)
73 except ValueError:
74 msg = "Unable to interpret string value '%s' as bytes" % (value)
75 raise ValueError(msg)
76 return size
77
78
79class BasicStringComparator(object):
80 """Provides a class that will compare strings from an iterator type object.
81 Used to provide > and < comparisons on strings that may not necessarily be
82 alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the
83 z-wrap.
84 """
85
86 _list = None
87
88 def __init__(self, item):
89 if self._list is None:
90 raise Exception("Must define the _list in the class definition!")
91 try:
92 self.index = self._list.index(item)
93 except Exception:
94 raise KeyError("Item '{}' is not in list '{}'"
95 .format(item, self._list))
96
97 def __eq__(self, other):
98 assert isinstance(other, str) or isinstance(other, self.__class__)
99 return self.index == self._list.index(other)
100
101 def __ne__(self, other):
102 return not self.__eq__(other)
103
104 def __lt__(self, other):
105 assert isinstance(other, str) or isinstance(other, self.__class__)
106 return self.index < self._list.index(other)
107
108 def __ge__(self, other):
109 return not self.__lt__(other)
110
111 def __gt__(self, other):
112 assert isinstance(other, str) or isinstance(other, self.__class__)
113 return self.index > self._list.index(other)
114
115 def __le__(self, other):
116 return not self.__gt__(other)
117
118 def __str__(self):
119 """Always give back the item at the index so it can be used in
120 comparisons like:
121
122 s_mitaka = CompareOpenStack('mitaka')
123 s_newton = CompareOpenstack('newton')
124
125 assert s_newton > s_mitaka
126
127 @returns: <string>
128 """
129 return self._list[self.index]
73130
=== modified file 'charmhelpers/core/sysctl.py'
--- charmhelpers/core/sysctl.py 2015-03-12 11:42:26 +0000
+++ charmhelpers/core/sysctl.py 2021-04-13 19:16:05 +0000
@@ -3,19 +3,17 @@
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1917
20import yaml18import yaml
2119
@@ -30,27 +28,38 @@
30__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'28__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
3129
3230
33def create(sysctl_dict, sysctl_file):31def create(sysctl_dict, sysctl_file, ignore=False):
34 """Creates a sysctl.conf file from a YAML associative array32 """Creates a sysctl.conf file from a YAML associative array
3533
36 :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"34 :param sysctl_dict: a dict or YAML-formatted string of sysctl
35 options eg "{ 'kernel.max_pid': 1337 }"
37 :type sysctl_dict: str36 :type sysctl_dict: str
38 :param sysctl_file: path to the sysctl file to be saved37 :param sysctl_file: path to the sysctl file to be saved
39 :type sysctl_file: str or unicode38 :type sysctl_file: str or unicode
39 :param ignore: If True, ignore "unknown variable" errors.
40 :type ignore: bool
40 :returns: None41 :returns: None
41 """42 """
42 try:43 if type(sysctl_dict) is not dict:
43 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)44 try:
44 except yaml.YAMLError:45 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
45 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),46 except yaml.YAMLError:
46 level=ERROR)47 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
47 return48 level=ERROR)
49 return
50 else:
51 sysctl_dict_parsed = sysctl_dict
4852
49 with open(sysctl_file, "w") as fd:53 with open(sysctl_file, "w") as fd:
50 for key, value in sysctl_dict_parsed.items():54 for key, value in sysctl_dict_parsed.items():
51 fd.write("{}={}\n".format(key, value))55 fd.write("{}={}\n".format(key, value))
5256
53 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),57 log("Updating sysctl_file: {} values: {}".format(sysctl_file,
58 sysctl_dict_parsed),
54 level=DEBUG)59 level=DEBUG)
5560
56 check_call(["sysctl", "-p", sysctl_file])61 call = ["sysctl", "-p", sysctl_file]
62 if ignore:
63 call.append("-e")
64
65 check_call(call)
5766
=== modified file 'charmhelpers/core/templating.py'
--- charmhelpers/core/templating.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/templating.py 2021-04-13 19:16:05 +0000
@@ -1,27 +1,27 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
16import sys
1817
19from charmhelpers.core import host18from charmhelpers.core import host
20from charmhelpers.core import hookenv19from charmhelpers.core import hookenv
2120
2221
23def render(source, target, context, owner='root', group='root',22def render(source, target, context, owner='root', group='root',
24 perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):23 perms=0o444, templates_dir=None, encoding='UTF-8',
24 template_loader=None, config_template=None):
25 """25 """
26 Render a template.26 Render a template.
2727
@@ -33,6 +33,9 @@
33 The context should be a dict containing the values to be replaced in the33 The context should be a dict containing the values to be replaced in the
34 template.34 template.
3535
36 config_template may be provided to render from a provided template instead
37 of loading from a file.
38
36 The `owner`, `group`, and `perms` options will be passed to `write_file`.39 The `owner`, `group`, and `perms` options will be passed to `write_file`.
3740
38 If omitted, `templates_dir` defaults to the `templates` folder in the charm.41 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
@@ -40,8 +43,9 @@
40 The rendered template will be written to the file as well as being returned43 The rendered template will be written to the file as well as being returned
41 as a string.44 as a string.
4245
43 Note: Using this requires python-jinja2; if it is not installed, calling46 Note: Using this requires python-jinja2 or python3-jinja2; if it is not
44 this will attempt to use charmhelpers.fetch.apt_install to install it.47 installed, calling this will attempt to use charmhelpers.fetch.apt_install
48 to install it.
45 """49 """
46 try:50 try:
47 from jinja2 import FileSystemLoader, Environment, exceptions51 from jinja2 import FileSystemLoader, Environment, exceptions
@@ -53,7 +57,10 @@
53 'charmhelpers.fetch to install it',57 'charmhelpers.fetch to install it',
54 level=hookenv.ERROR)58 level=hookenv.ERROR)
55 raise59 raise
56 apt_install('python-jinja2', fatal=True)60 if sys.version_info.major == 2:
61 apt_install('python-jinja2', fatal=True)
62 else:
63 apt_install('python3-jinja2', fatal=True)
57 from jinja2 import FileSystemLoader, Environment, exceptions64 from jinja2 import FileSystemLoader, Environment, exceptions
5865
59 if template_loader:66 if template_loader:
@@ -62,14 +69,19 @@
62 if templates_dir is None:69 if templates_dir is None:
63 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')70 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
64 template_env = Environment(loader=FileSystemLoader(templates_dir))71 template_env = Environment(loader=FileSystemLoader(templates_dir))
65 try:72
66 source = source73 # load from a string if provided explicitly
67 template = template_env.get_template(source)74 if config_template is not None:
68 except exceptions.TemplateNotFound as e:75 template = template_env.from_string(config_template)
69 hookenv.log('Could not load template %s from %s.' %76 else:
70 (source, templates_dir),77 try:
71 level=hookenv.ERROR)78 source = source
72 raise e79 template = template_env.get_template(source)
80 except exceptions.TemplateNotFound as e:
81 hookenv.log('Could not load template %s from %s.' %
82 (source, templates_dir),
83 level=hookenv.ERROR)
84 raise e
73 content = template.render(context)85 content = template.render(context)
74 if target is not None:86 if target is not None:
75 target_dir = os.path.dirname(target)87 target_dir = os.path.dirname(target)
7688
=== modified file 'charmhelpers/core/unitdata.py'
--- charmhelpers/core/unitdata.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/core/unitdata.py 2021-04-13 19:16:05 +0000
@@ -3,20 +3,17 @@
3#3#
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19#
20#17#
21# Authors:18# Authors:
22# Kapil Thangavelu <kapil.foss@gmail.com>19# Kapil Thangavelu <kapil.foss@gmail.com>
@@ -169,6 +166,10 @@
169166
170 To support dicts, lists, integer, floats, and booleans values167 To support dicts, lists, integer, floats, and booleans values
171 are automatically json encoded/decoded.168 are automatically json encoded/decoded.
169
170 Note: to facilitate unit testing, ':memory:' can be passed as the
171 path parameter which causes sqlite3 to only build the db in memory.
172 This should only be used for testing purposes.
172 """173 """
173 def __init__(self, path=None):174 def __init__(self, path=None):
174 self.db_path = path175 self.db_path = path
@@ -178,6 +179,9 @@
178 else:179 else:
179 self.db_path = os.path.join(180 self.db_path = os.path.join(
180 os.environ.get('CHARM_DIR', ''), '.unit-state.db')181 os.environ.get('CHARM_DIR', ''), '.unit-state.db')
182 if self.db_path != ':memory:':
183 with open(self.db_path, 'a') as f:
184 os.fchmod(f.fileno(), 0o600)
181 self.conn = sqlite3.connect('%s' % self.db_path)185 self.conn = sqlite3.connect('%s' % self.db_path)
182 self.cursor = self.conn.cursor()186 self.cursor = self.conn.cursor()
183 self.revision = None187 self.revision = None
@@ -361,7 +365,7 @@
361 try:365 try:
362 yield self.revision366 yield self.revision
363 self.revision = None367 self.revision = None
364 except:368 except Exception:
365 self.flush(False)369 self.flush(False)
366 self.revision = None370 self.revision = None
367 raise371 raise
368372
=== modified file 'charmhelpers/fetch/__init__.py'
--- charmhelpers/fetch/__init__.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/fetch/__init__.py 2021-04-13 19:16:05 +0000
@@ -1,32 +1,24 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import importlib15import importlib
18from tempfile import NamedTemporaryFile16from charmhelpers.osplatform import get_platform
19import time
20from yaml import safe_load17from yaml import safe_load
21from charmhelpers.core.host import (
22 lsb_release
23)
24import subprocess
25from charmhelpers.core.hookenv import (18from charmhelpers.core.hookenv import (
26 config,19 config,
27 log,20 log,
28)21)
29import os
3022
31import six23import six
32if six.PY3:24if six.PY3:
@@ -35,71 +27,6 @@
35 from urlparse import urlparse, urlunparse27 from urlparse import urlparse, urlunparse
3628
3729
38CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
39deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
40"""
41PROPOSED_POCKET = """# Proposed
42deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
43"""
44CLOUD_ARCHIVE_POCKETS = {
45 # Folsom
46 'folsom': 'precise-updates/folsom',
47 'precise-folsom': 'precise-updates/folsom',
48 'precise-folsom/updates': 'precise-updates/folsom',
49 'precise-updates/folsom': 'precise-updates/folsom',
50 'folsom/proposed': 'precise-proposed/folsom',
51 'precise-folsom/proposed': 'precise-proposed/folsom',
52 'precise-proposed/folsom': 'precise-proposed/folsom',
53 # Grizzly
54 'grizzly': 'precise-updates/grizzly',
55 'precise-grizzly': 'precise-updates/grizzly',
56 'precise-grizzly/updates': 'precise-updates/grizzly',
57 'precise-updates/grizzly': 'precise-updates/grizzly',
58 'grizzly/proposed': 'precise-proposed/grizzly',
59 'precise-grizzly/proposed': 'precise-proposed/grizzly',
60 'precise-proposed/grizzly': 'precise-proposed/grizzly',
61 # Havana
62 'havana': 'precise-updates/havana',
63 'precise-havana': 'precise-updates/havana',
64 'precise-havana/updates': 'precise-updates/havana',
65 'precise-updates/havana': 'precise-updates/havana',
66 'havana/proposed': 'precise-proposed/havana',
67 'precise-havana/proposed': 'precise-proposed/havana',
68 'precise-proposed/havana': 'precise-proposed/havana',
69 # Icehouse
70 'icehouse': 'precise-updates/icehouse',
71 'precise-icehouse': 'precise-updates/icehouse',
72 'precise-icehouse/updates': 'precise-updates/icehouse',
73 'precise-updates/icehouse': 'precise-updates/icehouse',
74 'icehouse/proposed': 'precise-proposed/icehouse',
75 'precise-icehouse/proposed': 'precise-proposed/icehouse',
76 'precise-proposed/icehouse': 'precise-proposed/icehouse',
77 # Juno
78 'juno': 'trusty-updates/juno',
79 'trusty-juno': 'trusty-updates/juno',
80 'trusty-juno/updates': 'trusty-updates/juno',
81 'trusty-updates/juno': 'trusty-updates/juno',
82 'juno/proposed': 'trusty-proposed/juno',
83 'trusty-juno/proposed': 'trusty-proposed/juno',
84 'trusty-proposed/juno': 'trusty-proposed/juno',
85 # Kilo
86 'kilo': 'trusty-updates/kilo',
87 'trusty-kilo': 'trusty-updates/kilo',
88 'trusty-kilo/updates': 'trusty-updates/kilo',
89 'trusty-updates/kilo': 'trusty-updates/kilo',
90 'kilo/proposed': 'trusty-proposed/kilo',
91 'trusty-kilo/proposed': 'trusty-proposed/kilo',
92 'trusty-proposed/kilo': 'trusty-proposed/kilo',
93 # Liberty
94 'liberty': 'trusty-updates/liberty',
95 'trusty-liberty': 'trusty-updates/liberty',
96 'trusty-liberty/updates': 'trusty-updates/liberty',
97 'trusty-updates/liberty': 'trusty-updates/liberty',
98 'liberty/proposed': 'trusty-proposed/liberty',
99 'trusty-liberty/proposed': 'trusty-proposed/liberty',
100 'trusty-proposed/liberty': 'trusty-proposed/liberty',
101}
102
103# The order of this list is very important. Handlers should be listed in from30# The order of this list is very important. Handlers should be listed in from
104# least- to most-specific URL matching.31# least- to most-specific URL matching.
105FETCH_HANDLERS = (32FETCH_HANDLERS = (
@@ -108,10 +35,6 @@
108 'charmhelpers.fetch.giturl.GitUrlFetchHandler',35 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
109)36)
11037
111APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
112APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
113APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
114
11538
116class SourceConfigError(Exception):39class SourceConfigError(Exception):
117 pass40 pass
@@ -125,6 +48,13 @@
125 pass48 pass
12649
12750
51class GPGKeyError(Exception):
52 """Exception occurs when a GPG key cannot be fetched or used. The message
53 indicates what the problem is.
54 """
55 pass
56
57
128class BaseFetchHandler(object):58class BaseFetchHandler(object):
12959
130 """Base class for FetchHandler implementations in fetch plugins"""60 """Base class for FetchHandler implementations in fetch plugins"""
@@ -149,180 +79,41 @@
149 return urlunparse(parts)79 return urlunparse(parts)
15080
15181
152def filter_installed_packages(packages):82__platform__ = get_platform()
153 """Returns a list of packages that require installation"""83module = "charmhelpers.fetch.%s" % __platform__
154 cache = apt_cache()84fetch = importlib.import_module(module)
155 _pkgs = []85
156 for package in packages:86filter_installed_packages = fetch.filter_installed_packages
157 try:87filter_missing_packages = fetch.filter_missing_packages
158 p = cache[package]88install = fetch.apt_install
159 p.current_ver or _pkgs.append(package)89upgrade = fetch.apt_upgrade
160 except KeyError:90update = _fetch_update = fetch.apt_update
161 log('Package {} has no installation candidate.'.format(package),91purge = fetch.apt_purge
162 level='WARNING')92add_source = fetch.add_source
163 _pkgs.append(package)93
164 return _pkgs94if __platform__ == "ubuntu":
16595 apt_cache = fetch.apt_cache
16696 apt_install = fetch.apt_install
167def apt_cache(in_memory=True):97 apt_update = fetch.apt_update
168 """Build and return an apt cache"""98 apt_upgrade = fetch.apt_upgrade
169 from apt import apt_pkg99 apt_purge = fetch.apt_purge
170 apt_pkg.init()100 apt_autoremove = fetch.apt_autoremove
171 if in_memory:101 apt_mark = fetch.apt_mark
172 apt_pkg.config.set("Dir::Cache::pkgcache", "")102 apt_hold = fetch.apt_hold
173 apt_pkg.config.set("Dir::Cache::srcpkgcache", "")103 apt_unhold = fetch.apt_unhold
174 return apt_pkg.Cache()104 import_key = fetch.import_key
175105 get_upstream_version = fetch.get_upstream_version
176106elif __platform__ == "centos":
177def apt_install(packages, options=None, fatal=False):107 yum_search = fetch.yum_search
178 """Install one or more packages"""
179 if options is None:
180 options = ['--option=Dpkg::Options::=--force-confold']
181
182 cmd = ['apt-get', '--assume-yes']
183 cmd.extend(options)
184 cmd.append('install')
185 if isinstance(packages, six.string_types):
186 cmd.append(packages)
187 else:
188 cmd.extend(packages)
189 log("Installing {} with options: {}".format(packages,
190 options))
191 _run_apt_command(cmd, fatal)
192
193
194def apt_upgrade(options=None, fatal=False, dist=False):
195 """Upgrade all packages"""
196 if options is None:
197 options = ['--option=Dpkg::Options::=--force-confold']
198
199 cmd = ['apt-get', '--assume-yes']
200 cmd.extend(options)
201 if dist:
202 cmd.append('dist-upgrade')
203 else:
204 cmd.append('upgrade')
205 log("Upgrading with options: {}".format(options))
206 _run_apt_command(cmd, fatal)
207
208
209def apt_update(fatal=False):
210 """Update local apt cache"""
211 cmd = ['apt-get', 'update']
212 _run_apt_command(cmd, fatal)
213
214
215def apt_purge(packages, fatal=False):
216 """Purge one or more packages"""
217 cmd = ['apt-get', '--assume-yes', 'purge']
218 if isinstance(packages, six.string_types):
219 cmd.append(packages)
220 else:
221 cmd.extend(packages)
222 log("Purging {}".format(packages))
223 _run_apt_command(cmd, fatal)
224
225
226def apt_mark(packages, mark, fatal=False):
227 """Flag one or more packages using apt-mark"""
228 log("Marking {} as {}".format(packages, mark))
229 cmd = ['apt-mark', mark]
230 if isinstance(packages, six.string_types):
231 cmd.append(packages)
232 else:
233 cmd.extend(packages)
234
235 if fatal:
236 subprocess.check_call(cmd, universal_newlines=True)
237 else:
238 subprocess.call(cmd, universal_newlines=True)
239
240
241def apt_hold(packages, fatal=False):
242 return apt_mark(packages, 'hold', fatal=fatal)
243
244
245def apt_unhold(packages, fatal=False):
246 return apt_mark(packages, 'unhold', fatal=fatal)
247
248
249def add_source(source, key=None):
250 """Add a package source to this system.
251
252 @param source: a URL or sources.list entry, as supported by
253 add-apt-repository(1). Examples::
254
255 ppa:charmers/example
256 deb https://stub:key@private.example.com/ubuntu trusty main
257
258 In addition:
259 'proposed:' may be used to enable the standard 'proposed'
260 pocket for the release.
261 'cloud:' may be used to activate official cloud archive pockets,
262 such as 'cloud:icehouse'
263 'distro' may be used as a noop
264
265 @param key: A key to be added to the system's APT keyring and used
266 to verify the signatures on packages. Ideally, this should be an
267 ASCII format GPG public key including the block headers. A GPG key
268 id may also be used, but be aware that only insecure protocols are
269 available to retrieve the actual public key from a public keyserver
270 placing your Juju environment at risk. ppa and cloud archive keys
271 are securely added automtically, so sould not be provided.
272 """
273 if source is None:
274 log('Source is not present. Skipping')
275 return
276
277 if (source.startswith('ppa:') or
278 source.startswith('http') or
279 source.startswith('deb ') or
280 source.startswith('cloud-archive:')):
281 subprocess.check_call(['add-apt-repository', '--yes', source])
282 elif source.startswith('cloud:'):
283 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
284 fatal=True)
285 pocket = source.split(':')[-1]
286 if pocket not in CLOUD_ARCHIVE_POCKETS:
287 raise SourceConfigError(
288 'Unsupported cloud: source option %s' %
289 pocket)
290 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
291 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
292 apt.write(CLOUD_ARCHIVE.format(actual_pocket))
293 elif source == 'proposed':
294 release = lsb_release()['DISTRIB_CODENAME']
295 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
296 apt.write(PROPOSED_POCKET.format(release))
297 elif source == 'distro':
298 pass
299 else:
300 log("Unknown source: {!r}".format(source))
301
302 if key:
303 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
304 with NamedTemporaryFile('w+') as key_file:
305 key_file.write(key)
306 key_file.flush()
307 key_file.seek(0)
308 subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
309 else:
310 # Note that hkp: is in no way a secure protocol. Using a
311 # GPG key id is pointless from a security POV unless you
312 # absolutely trust your network and DNS.
313 subprocess.check_call(['apt-key', 'adv', '--keyserver',
314 'hkp://keyserver.ubuntu.com:80', '--recv',
315 key])
316108
317109
318def configure_sources(update=False,110def configure_sources(update=False,
319 sources_var='install_sources',111 sources_var='install_sources',
320 keys_var='install_keys'):112 keys_var='install_keys'):
321 """113 """Configure multiple sources from charm configuration.
322 Configure multiple sources from charm configuration.
323114
324 The lists are encoded as yaml fragments in the configuration.115 The lists are encoded as yaml fragments in the configuration.
325 The frament needs to be included as a string. Sources and their116 The fragment needs to be included as a string. Sources and their
326 corresponding keys are of the types supported by add_source().117 corresponding keys are of the types supported by add_source().
327118
328 Example config:119 Example config:
@@ -354,12 +145,11 @@
354 for source, key in zip(sources, keys):145 for source, key in zip(sources, keys):
355 add_source(source, key)146 add_source(source, key)
356 if update:147 if update:
357 apt_update(fatal=True)148 _fetch_update(fatal=True)
358149
359150
360def install_remote(source, *args, **kwargs):151def install_remote(source, *args, **kwargs):
361 """152 """Install a file tree from a remote source.
362 Install a file tree from a remote source
363153
364 The specified source should be a url of the form:154 The specified source should be a url of the form:
365 scheme://[host]/path[#[option=value][&...]]155 scheme://[host]/path[#[option=value][&...]]
@@ -382,19 +172,17 @@
382 # We ONLY check for True here because can_handle may return a string172 # We ONLY check for True here because can_handle may return a string
383 # explaining why it can't handle a given source.173 # explaining why it can't handle a given source.
384 handlers = [h for h in plugins() if h.can_handle(source) is True]174 handlers = [h for h in plugins() if h.can_handle(source) is True]
385 installed_to = None
386 for handler in handlers:175 for handler in handlers:
387 try:176 try:
388 installed_to = handler.install(source, *args, **kwargs)177 return handler.install(source, *args, **kwargs)
389 except UnhandledSource as e:178 except UnhandledSource as e:
390 log('Install source attempt unsuccessful: {}'.format(e),179 log('Install source attempt unsuccessful: {}'.format(e),
391 level='WARNING')180 level='WARNING')
392 if not installed_to:181 raise UnhandledSource("No handler found for source {}".format(source))
393 raise UnhandledSource("No handler found for source {}".format(source))
394 return installed_to
395182
396183
397def install_from_config(config_var_name):184def install_from_config(config_var_name):
185 """Install a file from config."""
398 charm_config = config()186 charm_config = config()
399 source = charm_config[config_var_name]187 source = charm_config[config_var_name]
400 return install_remote(source)188 return install_remote(source)
@@ -411,46 +199,9 @@
411 importlib.import_module(package),199 importlib.import_module(package),
412 classname)200 classname)
413 plugin_list.append(handler_class())201 plugin_list.append(handler_class())
414 except (ImportError, AttributeError):202 except NotImplementedError:
415 # Skip missing plugins so that they can be ommitted from203 # Skip missing plugins so that they can be ommitted from
416 # installation if desired204 # installation if desired
417 log("FetchHandler {} not found, skipping plugin".format(205 log("FetchHandler {} not found, skipping plugin".format(
418 handler_name))206 handler_name))
419 return plugin_list207 return plugin_list
420
421
422def _run_apt_command(cmd, fatal=False):
423 """
424 Run an APT command, checking output and retrying if the fatal flag is set
425 to True.
426
427 :param: cmd: str: The apt command to run.
428 :param: fatal: bool: Whether the command's output should be checked and
429 retried.
430 """
431 env = os.environ.copy()
432
433 if 'DEBIAN_FRONTEND' not in env:
434 env['DEBIAN_FRONTEND'] = 'noninteractive'
435
436 if fatal:
437 retry_count = 0
438 result = None
439
440 # If the command is considered "fatal", we need to retry if the apt
441 # lock was not acquired.
442
443 while result is None or result == APT_NO_LOCK:
444 try:
445 result = subprocess.check_call(cmd, env=env)
446 except subprocess.CalledProcessError as e:
447 retry_count = retry_count + 1
448 if retry_count > APT_NO_LOCK_RETRY_COUNT:
449 raise
450 result = e.returncode
451 log("Couldn't acquire DPKG lock. Will retry in {} seconds."
452 "".format(APT_NO_LOCK_RETRY_DELAY))
453 time.sleep(APT_NO_LOCK_RETRY_DELAY)
454
455 else:
456 subprocess.call(cmd, env=env)
457208
=== modified file 'charmhelpers/fetch/archiveurl.py'
--- charmhelpers/fetch/archiveurl.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/fetch/archiveurl.py 2021-04-13 19:16:05 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
18import hashlib16import hashlib
@@ -91,7 +89,7 @@
91 :param str source: URL pointing to an archive file.89 :param str source: URL pointing to an archive file.
92 :param str dest: Local path location to download archive file to.90 :param str dest: Local path location to download archive file to.
93 """91 """
94 # propogate all exceptions92 # propagate all exceptions
95 # URLError, OSError, etc93 # URLError, OSError, etc
96 proto, netloc, path, params, query, fragment = urlparse(source)94 proto, netloc, path, params, query, fragment = urlparse(source)
97 if proto in ('http', 'https'):95 if proto in ('http', 'https'):
9896
=== modified file 'charmhelpers/fetch/bzrurl.py'
--- charmhelpers/fetch/bzrurl.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/fetch/bzrurl.py 2021-04-13 19:16:05 +0000
@@ -1,70 +1,63 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
16from subprocess import STDOUT, check_output
18from charmhelpers.fetch import (17from charmhelpers.fetch import (
19 BaseFetchHandler,18 BaseFetchHandler,
20 UnhandledSource19 UnhandledSource,
20 filter_installed_packages,
21 install,
21)22)
22from charmhelpers.core.host import mkdir23from charmhelpers.core.host import mkdir
2324
24import six
25if six.PY3:
26 raise ImportError('bzrlib does not support Python3')
2725
28try:26if filter_installed_packages(['bzr']) != []:
29 from bzrlib.branch import Branch27 install(['bzr'])
30 from bzrlib import bzrdir, workingtree, errors28 if filter_installed_packages(['bzr']) != []:
31except ImportError:29 raise NotImplementedError('Unable to install bzr')
32 from charmhelpers.fetch import apt_install
33 apt_install("python-bzrlib")
34 from bzrlib.branch import Branch
35 from bzrlib import bzrdir, workingtree, errors
3630
3731
38class BzrUrlFetchHandler(BaseFetchHandler):32class BzrUrlFetchHandler(BaseFetchHandler):
39 """Handler for bazaar branches via generic and lp URLs"""33 """Handler for bazaar branches via generic and lp URLs."""
34
40 def can_handle(self, source):35 def can_handle(self, source):
41 url_parts = self.parse_url(source)36 url_parts = self.parse_url(source)
42 if url_parts.scheme not in ('bzr+ssh', 'lp'):37 if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
43 return False38 return False
39 elif not url_parts.scheme:
40 return os.path.exists(os.path.join(source, '.bzr'))
44 else:41 else:
45 return True42 return True
4643
47 def branch(self, source, dest):44 def branch(self, source, dest, revno=None):
48 url_parts = self.parse_url(source)
49 # If we use lp:branchname scheme we need to load plugins
50 if not self.can_handle(source):45 if not self.can_handle(source):
51 raise UnhandledSource("Cannot handle {}".format(source))46 raise UnhandledSource("Cannot handle {}".format(source))
52 if url_parts.scheme == "lp":47 cmd_opts = []
53 from bzrlib.plugin import load_plugins48 if revno:
54 load_plugins()49 cmd_opts += ['-r', str(revno)]
55 try:50 if os.path.exists(dest):
56 local_branch = bzrdir.BzrDir.create_branch_convenience(dest)51 cmd = ['bzr', 'pull']
57 except errors.AlreadyControlDirError:52 cmd += cmd_opts
58 local_branch = Branch.open(dest)53 cmd += ['--overwrite', '-d', dest, source]
59 try:54 else:
60 remote_branch = Branch.open(source)55 cmd = ['bzr', 'branch']
61 remote_branch.push(local_branch)56 cmd += cmd_opts
62 tree = workingtree.WorkingTree.open(dest)57 cmd += [source, dest]
63 tree.update()58 check_output(cmd, stderr=STDOUT)
64 except Exception as e:
65 raise e
6659
67 def install(self, source, dest=None):60 def install(self, source, dest=None, revno=None):
68 url_parts = self.parse_url(source)61 url_parts = self.parse_url(source)
69 branch_name = url_parts.path.strip("/").split("/")[-1]62 branch_name = url_parts.path.strip("/").split("/")[-1]
70 if dest:63 if dest:
@@ -73,10 +66,11 @@
73 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",66 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
74 branch_name)67 branch_name)
7568
76 if not os.path.exists(dest_dir):69 if dest and not os.path.exists(dest):
77 mkdir(dest_dir, perms=0o755)70 mkdir(dest, perms=0o755)
71
78 try:72 try:
79 self.branch(source, dest_dir)73 self.branch(source, dest_dir, revno)
80 except OSError as e:74 except OSError as e:
81 raise UnhandledSource(e.strerror)75 raise UnhandledSource(e.strerror)
82 return dest_dir76 return dest_dir
8377
=== added file 'charmhelpers/fetch/centos.py'
--- charmhelpers/fetch/centos.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/centos.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,171 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import subprocess
16import os
17import time
18import six
19import yum
20
21from tempfile import NamedTemporaryFile
22from charmhelpers.core.hookenv import log
23
24YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM.
25YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
26YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
27
28
29def filter_installed_packages(packages):
30 """Return a list of packages that require installation."""
31 yb = yum.YumBase()
32 package_list = yb.doPackageLists()
33 temp_cache = {p.base_package_name: 1 for p in package_list['installed']}
34
35 _pkgs = [p for p in packages if not temp_cache.get(p, False)]
36 return _pkgs
37
38
39def install(packages, options=None, fatal=False):
40 """Install one or more packages."""
41 cmd = ['yum', '--assumeyes']
42 if options is not None:
43 cmd.extend(options)
44 cmd.append('install')
45 if isinstance(packages, six.string_types):
46 cmd.append(packages)
47 else:
48 cmd.extend(packages)
49 log("Installing {} with options: {}".format(packages,
50 options))
51 _run_yum_command(cmd, fatal)
52
53
54def upgrade(options=None, fatal=False, dist=False):
55 """Upgrade all packages."""
56 cmd = ['yum', '--assumeyes']
57 if options is not None:
58 cmd.extend(options)
59 cmd.append('upgrade')
60 log("Upgrading with options: {}".format(options))
61 _run_yum_command(cmd, fatal)
62
63
64def update(fatal=False):
65 """Update local yum cache."""
66 cmd = ['yum', '--assumeyes', 'update']
67 log("Update with fatal: {}".format(fatal))
68 _run_yum_command(cmd, fatal)
69
70
71def purge(packages, fatal=False):
72 """Purge one or more packages."""
73 cmd = ['yum', '--assumeyes', 'remove']
74 if isinstance(packages, six.string_types):
75 cmd.append(packages)
76 else:
77 cmd.extend(packages)
78 log("Purging {}".format(packages))
79 _run_yum_command(cmd, fatal)
80
81
82def yum_search(packages):
83 """Search for a package."""
84 output = {}
85 cmd = ['yum', 'search']
86 if isinstance(packages, six.string_types):
87 cmd.append(packages)
88 else:
89 cmd.extend(packages)
90 log("Searching for {}".format(packages))
91 result = subprocess.check_output(cmd)
92 for package in list(packages):
93 output[package] = package in result
94 return output
95
96
97def add_source(source, key=None):
98 """Add a package source to this system.
99
100 @param source: a URL with a rpm package
101
102 @param key: A key to be added to the system's keyring and used
103 to verify the signatures on packages. Ideally, this should be an
104 ASCII format GPG public key including the block headers. A GPG key
105 id may also be used, but be aware that only insecure protocols are
106 available to retrieve the actual public key from a public keyserver
107 placing your Juju environment at risk.
108 """
109 if source is None:
110 log('Source is not present. Skipping')
111 return
112
113 if source.startswith('http'):
114 directory = '/etc/yum.repos.d/'
115 for filename in os.listdir(directory):
116 with open(directory + filename, 'r') as rpm_file:
117 if source in rpm_file.read():
118 break
119 else:
120 log("Add source: {!r}".format(source))
121 # write in the charms.repo
122 with open(directory + 'Charms.repo', 'a') as rpm_file:
123 rpm_file.write('[%s]\n' % source[7:].replace('/', '_'))
124 rpm_file.write('name=%s\n' % source[7:])
125 rpm_file.write('baseurl=%s\n\n' % source)
126 else:
127 log("Unknown source: {!r}".format(source))
128
129 if key:
130 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
131 with NamedTemporaryFile('w+') as key_file:
132 key_file.write(key)
133 key_file.flush()
134 key_file.seek(0)
135 subprocess.check_call(['rpm', '--import', key_file.name])
136 else:
137 subprocess.check_call(['rpm', '--import', key])
138
139
140def _run_yum_command(cmd, fatal=False):
141 """Run an YUM command.
142
143 Checks the output and retry if the fatal flag is set to True.
144
145 :param: cmd: str: The yum command to run.
146 :param: fatal: bool: Whether the command's output should be checked and
147 retried.
148 """
149 env = os.environ.copy()
150
151 if fatal:
152 retry_count = 0
153 result = None
154
155 # If the command is considered "fatal", we need to retry if the yum
156 # lock was not acquired.
157
158 while result is None or result == YUM_NO_LOCK:
159 try:
160 result = subprocess.check_call(cmd, env=env)
161 except subprocess.CalledProcessError as e:
162 retry_count = retry_count + 1
163 if retry_count > YUM_NO_LOCK_RETRY_COUNT:
164 raise
165 result = e.returncode
166 log("Couldn't acquire YUM lock. Will retry in {} seconds."
167 "".format(YUM_NO_LOCK_RETRY_DELAY))
168 time.sleep(YUM_NO_LOCK_RETRY_DELAY)
169
170 else:
171 subprocess.call(cmd, env=env)
0172
=== modified file 'charmhelpers/fetch/giturl.py'
--- charmhelpers/fetch/giturl.py 2015-12-11 15:23:38 +0000
+++ charmhelpers/fetch/giturl.py 2021-04-13 19:16:05 +0000
@@ -1,54 +1,56 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
16from subprocess import check_output, CalledProcessError, STDOUT
18from charmhelpers.fetch import (17from charmhelpers.fetch import (
19 BaseFetchHandler,18 BaseFetchHandler,
20 UnhandledSource19 UnhandledSource,
20 filter_installed_packages,
21 install,
21)22)
22from charmhelpers.core.host import mkdir23
2324if filter_installed_packages(['git']) != []:
24try:25 install(['git'])
25 from git import Repo26 if filter_installed_packages(['git']) != []:
26except ImportError:27 raise NotImplementedError('Unable to install git')
27 from charmhelpers.fetch import apt_install
28 apt_install("python-git")
29 from git import Repo
30
31from git.exc import GitCommandError # noqa E402
3228
3329
34class GitUrlFetchHandler(BaseFetchHandler):30class GitUrlFetchHandler(BaseFetchHandler):
35 """Handler for git branches via generic and github URLs"""31 """Handler for git branches via generic and github URLs."""
32
36 def can_handle(self, source):33 def can_handle(self, source):
37 url_parts = self.parse_url(source)34 url_parts = self.parse_url(source)
38 # TODO (mattyw) no support for ssh git@ yet35 # TODO (mattyw) no support for ssh git@ yet
39 if url_parts.scheme not in ('http', 'https', 'git'):36 if url_parts.scheme not in ('http', 'https', 'git', ''):
40 return False37 return False
38 elif not url_parts.scheme:
39 return os.path.exists(os.path.join(source, '.git'))
41 else:40 else:
42 return True41 return True
4342
44 def clone(self, source, dest, branch, depth=None):43 def clone(self, source, dest, branch="master", depth=None):
45 if not self.can_handle(source):44 if not self.can_handle(source):
46 raise UnhandledSource("Cannot handle {}".format(source))45 raise UnhandledSource("Cannot handle {}".format(source))
4746
48 if depth:47 if os.path.exists(dest):
49 Repo.clone_from(source, dest, branch=branch, depth=depth)48 cmd = ['git', '-C', dest, 'pull', source, branch]
50 else:49 else:
51 Repo.clone_from(source, dest, branch=branch)50 cmd = ['git', 'clone', source, dest, '--branch', branch]
51 if depth:
52 cmd.extend(['--depth', depth])
53 check_output(cmd, stderr=STDOUT)
5254
53 def install(self, source, branch="master", dest=None, depth=None):55 def install(self, source, branch="master", dest=None, depth=None):
54 url_parts = self.parse_url(source)56 url_parts = self.parse_url(source)
@@ -58,11 +60,9 @@
58 else:60 else:
59 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",61 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
60 branch_name)62 branch_name)
61 if not os.path.exists(dest_dir):
62 mkdir(dest_dir, perms=0o755)
63 try:63 try:
64 self.clone(source, dest_dir, branch, depth)64 self.clone(source, dest_dir, branch, depth)
65 except GitCommandError as e:65 except CalledProcessError as e:
66 raise UnhandledSource(e)66 raise UnhandledSource(e)
67 except OSError as e:67 except OSError as e:
68 raise UnhandledSource(e.strerror)68 raise UnhandledSource(e.strerror)
6969
=== added directory 'charmhelpers/fetch/python'
=== added file 'charmhelpers/fetch/python/__init__.py'
--- charmhelpers/fetch/python/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/python/__init__.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,13 @@
1# Copyright 2014-2019 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
014
=== added file 'charmhelpers/fetch/python/debug.py'
--- charmhelpers/fetch/python/debug.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/python/debug.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,54 @@
1#!/usr/bin/env python
2# coding: utf-8
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18from __future__ import print_function
19
20import atexit
21import sys
22
23from charmhelpers.fetch.python.rpdb import Rpdb
24from charmhelpers.core.hookenv import (
25 open_port,
26 close_port,
27 ERROR,
28 log
29)
30
31__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
32
33DEFAULT_ADDR = "0.0.0.0"
34DEFAULT_PORT = 4444
35
36
37def _error(message):
38 log(message, level=ERROR)
39
40
41def set_trace(addr=DEFAULT_ADDR, port=DEFAULT_PORT):
42 """
43 Set a trace point using the remote debugger
44 """
45 atexit.register(close_port, port)
46 try:
47 log("Starting a remote python debugger session on %s:%s" % (addr,
48 port))
49 open_port(port)
50 debugger = Rpdb(addr=addr, port=port)
51 debugger.set_trace(sys._getframe().f_back)
52 except Exception:
53 _error("Cannot start a remote debug session on %s:%s" % (addr,
54 port))
055
=== added file 'charmhelpers/fetch/python/packages.py'
--- charmhelpers/fetch/python/packages.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/python/packages.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,154 @@
1#!/usr/bin/env python
2# coding: utf-8
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import os
19import six
20import subprocess
21import sys
22
23from charmhelpers.fetch import apt_install, apt_update
24from charmhelpers.core.hookenv import charm_dir, log
25
26__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
27
28
29def pip_execute(*args, **kwargs):
30 """Overriden pip_execute() to stop sys.path being changed.
31
32 The act of importing main from the pip module seems to cause add wheels
33 from the /usr/share/python-wheels which are installed by various tools.
34 This function ensures that sys.path remains the same after the call is
35 executed.
36 """
37 try:
38 _path = sys.path
39 try:
40 from pip import main as _pip_execute
41 except ImportError:
42 apt_update()
43 if six.PY2:
44 apt_install('python-pip')
45 else:
46 apt_install('python3-pip')
47 from pip import main as _pip_execute
48 _pip_execute(*args, **kwargs)
49 finally:
50 sys.path = _path
51
52
53def parse_options(given, available):
54 """Given a set of options, check if available"""
55 for key, value in sorted(given.items()):
56 if not value:
57 continue
58 if key in available:
59 yield "--{0}={1}".format(key, value)
60
61
62def pip_install_requirements(requirements, constraints=None, **options):
63 """Install a requirements file.
64
65 :param constraints: Path to pip constraints file.
66 http://pip.readthedocs.org/en/stable/user_guide/#constraints-files
67 """
68 command = ["install"]
69
70 available_options = ('proxy', 'src', 'log', )
71 for option in parse_options(options, available_options):
72 command.append(option)
73
74 command.append("-r {0}".format(requirements))
75 if constraints:
76 command.append("-c {0}".format(constraints))
77 log("Installing from file: {} with constraints {} "
78 "and options: {}".format(requirements, constraints, command))
79 else:
80 log("Installing from file: {} with options: {}".format(requirements,
81 command))
82 pip_execute(command)
83
84
85def pip_install(package, fatal=False, upgrade=False, venv=None,
86 constraints=None, **options):
87 """Install a python package"""
88 if venv:
89 venv_python = os.path.join(venv, 'bin/pip')
90 command = [venv_python, "install"]
91 else:
92 command = ["install"]
93
94 available_options = ('proxy', 'src', 'log', 'index-url', )
95 for option in parse_options(options, available_options):
96 command.append(option)
97
98 if upgrade:
99 command.append('--upgrade')
100
101 if constraints:
102 command.extend(['-c', constraints])
103
104 if isinstance(package, list):
105 command.extend(package)
106 else:
107 command.append(package)
108
109 log("Installing {} package with options: {}".format(package,
110 command))
111 if venv:
112 subprocess.check_call(command)
113 else:
114 pip_execute(command)
115
116
117def pip_uninstall(package, **options):
118 """Uninstall a python package"""
119 command = ["uninstall", "-q", "-y"]
120
121 available_options = ('proxy', 'log', )
122 for option in parse_options(options, available_options):
123 command.append(option)
124
125 if isinstance(package, list):
126 command.extend(package)
127 else:
128 command.append(package)
129
130 log("Uninstalling {} package with options: {}".format(package,
131 command))
132 pip_execute(command)
133
134
135def pip_list():
136 """Returns the list of current python installed packages
137 """
138 return pip_execute(["list"])
139
140
141def pip_create_virtualenv(path=None):
142 """Create an isolated Python environment."""
143 if six.PY2:
144 apt_install('python-virtualenv')
145 else:
146 apt_install('python3-virtualenv')
147
148 if path:
149 venv_path = path
150 else:
151 venv_path = os.path.join(charm_dir(), 'venv')
152
153 if not os.path.exists(venv_path):
154 subprocess.check_call(['virtualenv', venv_path])
0155
=== added file 'charmhelpers/fetch/python/rpdb.py'
--- charmhelpers/fetch/python/rpdb.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/python/rpdb.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,56 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Remote Python Debugger (pdb wrapper)."""
16
17import pdb
18import socket
19import sys
20
21__author__ = "Bertrand Janin <b@janin.com>"
22__version__ = "0.1.3"
23
24
25class Rpdb(pdb.Pdb):
26
27 def __init__(self, addr="127.0.0.1", port=4444):
28 """Initialize the socket and initialize pdb."""
29
30 # Backup stdin and stdout before replacing them by the socket handle
31 self.old_stdout = sys.stdout
32 self.old_stdin = sys.stdin
33
34 # Open a 'reusable' socket to let the webapp reload on the same port
35 self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
36 self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
37 self.skt.bind((addr, port))
38 self.skt.listen(1)
39 (clientsocket, address) = self.skt.accept()
40 handle = clientsocket.makefile('rw')
41 pdb.Pdb.__init__(self, completekey='tab', stdin=handle, stdout=handle)
42 sys.stdout = sys.stdin = handle
43
44 def shutdown(self):
45 """Revert stdin and stdout, close the socket."""
46 sys.stdout = self.old_stdout
47 sys.stdin = self.old_stdin
48 self.skt.close()
49 self.set_continue()
50
51 def do_continue(self, arg):
52 """Stop all operation on ``continue``."""
53 self.shutdown()
54 return 1
55
56 do_EOF = do_quit = do_exit = do_c = do_cont = do_continue
057
=== added file 'charmhelpers/fetch/python/version.py'
--- charmhelpers/fetch/python/version.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/python/version.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,32 @@
1#!/usr/bin/env python
2# coding: utf-8
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import sys
19
20__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
21
22
23def current_version():
24 """Current system python version"""
25 return sys.version_info
26
27
28def current_version_string():
29 """Current system python version as string major.minor.micro"""
30 return "{0}.{1}.{2}".format(sys.version_info.major,
31 sys.version_info.minor,
32 sys.version_info.micro)
033
=== added file 'charmhelpers/fetch/snap.py'
--- charmhelpers/fetch/snap.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/snap.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,150 @@
1# Copyright 2014-2017 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""
15Charm helpers snap for classic charms.
16
17If writing reactive charms, use the snap layer:
18https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html
19"""
20import subprocess
21import os
22from time import sleep
23from charmhelpers.core.hookenv import log
24
25__author__ = 'Joseph Borg <joseph.borg@canonical.com>'
26
27# The return code for "couldn't acquire lock" in Snap
28# (hopefully this will be improved).
29SNAP_NO_LOCK = 1
30SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks.
31SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
32SNAP_CHANNELS = [
33 'edge',
34 'beta',
35 'candidate',
36 'stable',
37]
38
39
40class CouldNotAcquireLockException(Exception):
41 pass
42
43
44class InvalidSnapChannel(Exception):
45 pass
46
47
48def _snap_exec(commands):
49 """
50 Execute snap commands.
51
52 :param commands: List commands
53 :return: Integer exit code
54 """
55 assert type(commands) == list
56
57 retry_count = 0
58 return_code = None
59
60 while return_code is None or return_code == SNAP_NO_LOCK:
61 try:
62 return_code = subprocess.check_call(['snap'] + commands,
63 env=os.environ)
64 except subprocess.CalledProcessError as e:
65 retry_count += + 1
66 if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
67 raise CouldNotAcquireLockException(
68 'Could not aquire lock after {} attempts'
69 .format(SNAP_NO_LOCK_RETRY_COUNT))
70 return_code = e.returncode
71 log('Snap failed to acquire lock, trying again in {} seconds.'
72 .format(SNAP_NO_LOCK_RETRY_DELAY, level='WARN'))
73 sleep(SNAP_NO_LOCK_RETRY_DELAY)
74
75 return return_code
76
77
78def snap_install(packages, *flags):
79 """
80 Install a snap package.
81
82 :param packages: String or List String package name
83 :param flags: List String flags to pass to install command
84 :return: Integer return code from snap
85 """
86 if type(packages) is not list:
87 packages = [packages]
88
89 flags = list(flags)
90
91 message = 'Installing snap(s) "%s"' % ', '.join(packages)
92 if flags:
93 message += ' with option(s) "%s"' % ', '.join(flags)
94
95 log(message, level='INFO')
96 return _snap_exec(['install'] + flags + packages)
97
98
99def snap_remove(packages, *flags):
100 """
101 Remove a snap package.
102
103 :param packages: String or List String package name
104 :param flags: List String flags to pass to remove command
105 :return: Integer return code from snap
106 """
107 if type(packages) is not list:
108 packages = [packages]
109
110 flags = list(flags)
111
112 message = 'Removing snap(s) "%s"' % ', '.join(packages)
113 if flags:
114 message += ' with options "%s"' % ', '.join(flags)
115
116 log(message, level='INFO')
117 return _snap_exec(['remove'] + flags + packages)
118
119
120def snap_refresh(packages, *flags):
121 """
122 Refresh / Update snap package.
123
124 :param packages: String or List String package name
125 :param flags: List String flags to pass to refresh command
126 :return: Integer return code from snap
127 """
128 if type(packages) is not list:
129 packages = [packages]
130
131 flags = list(flags)
132
133 message = 'Refreshing snap(s) "%s"' % ', '.join(packages)
134 if flags:
135 message += ' with options "%s"' % ', '.join(flags)
136
137 log(message, level='INFO')
138 return _snap_exec(['refresh'] + flags + packages)
139
140
141def valid_snap_channel(channel):
142 """ Validate snap channel exists
143
144 :raises InvalidSnapChannel: When channel does not exist
145 :return: Boolean
146 """
147 if channel.lower() in SNAP_CHANNELS:
148 return True
149 else:
150 raise InvalidSnapChannel("Invalid Snap Channel: {}".format(channel))
0151
=== added file 'charmhelpers/fetch/ubuntu.py'
--- charmhelpers/fetch/ubuntu.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/ubuntu.py 2021-04-13 19:16:05 +0000
@@ -0,0 +1,730 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches