Merge lp:~james-page/charms/trusty/neutron-openvswitch/lp1515008-stable into lp:~gnuoy/charms/trusty/neutron-openvswitch/neutron-refactor

Proposed by James Page
Status: Superseded
Proposed branch: lp:~james-page/charms/trusty/neutron-openvswitch/lp1515008-stable
Merge into: lp:~gnuoy/charms/trusty/neutron-openvswitch/neutron-refactor
Diff against target: 16850 lines (+12934/-1250)
122 files modified
.bzrignore (+2/-0)
.project (+17/-0)
.pydevproject (+9/-0)
Makefile (+21/-6)
README.md (+134/-18)
actions.yaml (+2/-0)
actions/git_reinstall.py (+45/-0)
charm-helpers-hooks.yaml (+13/-0)
charm-helpers-sync.yaml (+0/-10)
charm-helpers-tests.yaml (+5/-0)
config.yaml (+101/-16)
hooks/charmhelpers/__init__.py (+38/-0)
hooks/charmhelpers/cli/__init__.py (+191/-0)
hooks/charmhelpers/cli/benchmark.py (+36/-0)
hooks/charmhelpers/cli/commands.py (+32/-0)
hooks/charmhelpers/cli/hookenv.py (+23/-0)
hooks/charmhelpers/cli/host.py (+31/-0)
hooks/charmhelpers/cli/unitdata.py (+39/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hahelpers/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hahelpers/apache.py (+26/-3)
hooks/charmhelpers/contrib/hahelpers/ceph.py (+0/-297)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+172/-39)
hooks/charmhelpers/contrib/network/__init__.py (+15/-0)
hooks/charmhelpers/contrib/network/ip.py (+456/-0)
hooks/charmhelpers/contrib/network/ovs/__init__.py (+22/-1)
hooks/charmhelpers/contrib/openstack/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/alternatives.py (+16/-0)
hooks/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+197/-0)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+963/-0)
hooks/charmhelpers/contrib/openstack/context.py (+963/-236)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh (+32/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh (+30/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+151/-0)
hooks/charmhelpers/contrib/openstack/neutron.py (+189/-4)
hooks/charmhelpers/contrib/openstack/templates/__init__.py (+16/-0)
hooks/charmhelpers/contrib/openstack/templates/ceph.conf (+12/-6)
hooks/charmhelpers/contrib/openstack/templates/git.upstart (+17/-0)
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg (+30/-8)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend (+9/-8)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf (+9/-8)
hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken (+9/-0)
hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo (+22/-0)
hooks/charmhelpers/contrib/openstack/templates/section-zeromq (+14/-0)
hooks/charmhelpers/contrib/openstack/templating.py (+74/-31)
hooks/charmhelpers/contrib/openstack/utils.py (+631/-104)
hooks/charmhelpers/contrib/python/__init__.py (+15/-0)
hooks/charmhelpers/contrib/python/packages.py (+121/-0)
hooks/charmhelpers/contrib/storage/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+388/-118)
hooks/charmhelpers/contrib/storage/linux/loopback.py (+19/-3)
hooks/charmhelpers/contrib/storage/linux/lvm.py (+18/-1)
hooks/charmhelpers/contrib/storage/linux/utils.py (+44/-8)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/fstab.py (+134/-0)
hooks/charmhelpers/core/hookenv.py (+566/-37)
hooks/charmhelpers/core/host.py (+342/-53)
hooks/charmhelpers/core/hugepage.py (+69/-0)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/__init__.py (+18/-0)
hooks/charmhelpers/core/services/base.py (+353/-0)
hooks/charmhelpers/core/services/helpers.py (+283/-0)
hooks/charmhelpers/core/strutils.py (+72/-0)
hooks/charmhelpers/core/sysctl.py (+56/-0)
hooks/charmhelpers/core/templating.py (+68/-0)
hooks/charmhelpers/core/unitdata.py (+521/-0)
hooks/charmhelpers/fetch/__init__.py (+255/-107)
hooks/charmhelpers/fetch/archiveurl.py (+121/-17)
hooks/charmhelpers/fetch/bzrurl.py (+32/-3)
hooks/charmhelpers/fetch/giturl.py (+73/-0)
hooks/charmhelpers/payload/__init__.py (+16/-0)
hooks/charmhelpers/payload/execd.py (+16/-0)
hooks/neutron_ovs_context.py (+102/-30)
hooks/neutron_ovs_hooks.py (+87/-9)
hooks/neutron_ovs_utils.py (+339/-2)
metadata.yaml (+17/-3)
templates/ext-port.conf (+16/-0)
templates/git/neutron_sudoers (+4/-0)
templates/git/upstart/neutron-ovs-cleanup.upstart (+17/-0)
templates/git/upstart/neutron-plugin-openvswitch-agent.upstart (+18/-0)
templates/icehouse/dhcp_agent.ini (+14/-0)
templates/icehouse/metadata_agent.ini (+20/-0)
templates/icehouse/ml2_conf.ini (+16/-5)
templates/icehouse/neutron.conf (+6/-3)
templates/juno/fwaas_driver.ini (+7/-0)
templates/juno/l3_agent.ini (+7/-0)
templates/juno/metadata_agent.ini (+20/-0)
templates/juno/ml2_conf.ini (+43/-0)
templates/kilo/fwaas_driver.ini (+8/-0)
templates/kilo/neutron.conf (+42/-0)
templates/os-charm-phy-nic-mtu.conf (+22/-0)
tests/00-setup (+17/-0)
tests/014-basic-precise-icehouse (+11/-0)
tests/015-basic-trusty-icehouse (+9/-0)
tests/016-basic-trusty-juno (+11/-0)
tests/017-basic-trusty-kilo (+11/-0)
tests/019-basic-vivid-kilo (+9/-0)
tests/050-basic-trusty-icehouse-git (+9/-0)
tests/051-basic-trusty-juno-git (+12/-0)
tests/052-basic-trusty-kilo-git (+12/-0)
tests/README (+53/-0)
tests/basic_deployment.py (+256/-0)
tests/charmhelpers/__init__.py (+38/-0)
tests/charmhelpers/contrib/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+95/-0)
tests/charmhelpers/contrib/amulet/utils.py (+818/-0)
tests/charmhelpers/contrib/openstack/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+197/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+963/-0)
tests/tests.yaml (+20/-0)
unit_tests/__init__.py (+2/-0)
unit_tests/test_actions_git_reinstall.py (+105/-0)
unit_tests/test_neutron_ovs_context.py (+246/-20)
unit_tests/test_neutron_ovs_hooks.py (+154/-17)
unit_tests/test_neutron_ovs_utils.py (+314/-19)
To merge this branch: bzr merge lp:~james-page/charms/trusty/neutron-openvswitch/lp1515008-stable
Reviewer Review Type Date Requested Status
Liam Young Pending
Review via email: mp+277336@code.launchpad.net

Description of the change

Fixup handling of dvr and local dhcp configurations

To post a comment you must log in.

Unmerged revisions

73. By James Page

Fixup handling of dvr and local dhcp configurations

72. By Corey Bryant

[beisner,r=corey.bryant] Enable stable amulet tests and stable charm-helper syncs.

71. By James Page

15.10 Charm release

70. By Liam Young

Charmhelper sync

69. By Corey Bryant

[beisner,r=corey.bryant] Point charmhelper sync and amulet tests at stable branches.

68. By James Page

[gnuoy] 15.07 Charm release

67. By Corey Bryant

[corey.bryant,trivial] Update deploy from source README indentation.

66. By Corey Bryant

[corey.bryant,trivial] Update deploy from source README samples.

65. By Corey Bryant

[corey.bryant,trivial] Fix deploy from source README

64. By Liam Young

Point charmhelper sync and amulet tests at stable branches

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2014-06-19 09:56:25 +0000
3+++ .bzrignore 2015-11-12 11:46:11 +0000
4@@ -1,1 +1,3 @@
5+bin
6 .coverage
7+tags
8
9=== added file '.project'
10--- .project 1970-01-01 00:00:00 +0000
11+++ .project 2015-11-12 11:46:11 +0000
12@@ -0,0 +1,17 @@
13+<?xml version="1.0" encoding="UTF-8"?>
14+<projectDescription>
15+ <name>neutron-openvswitch</name>
16+ <comment></comment>
17+ <projects>
18+ </projects>
19+ <buildSpec>
20+ <buildCommand>
21+ <name>org.python.pydev.PyDevBuilder</name>
22+ <arguments>
23+ </arguments>
24+ </buildCommand>
25+ </buildSpec>
26+ <natures>
27+ <nature>org.python.pydev.pythonNature</nature>
28+ </natures>
29+</projectDescription>
30
31=== added file '.pydevproject'
32--- .pydevproject 1970-01-01 00:00:00 +0000
33+++ .pydevproject 2015-11-12 11:46:11 +0000
34@@ -0,0 +1,9 @@
35+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
36+<?eclipse-pydev version="1.0"?><pydev_project>
37+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
38+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
39+<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
40+<path>/neutron-openvswitch/hooks</path>
41+<path>/neutron-openvswitch/unit_tests</path>
42+</pydev_pathproperty>
43+</pydev_project>
44
45=== modified file 'Makefile'
46--- Makefile 2014-06-19 09:56:25 +0000
47+++ Makefile 2015-11-12 11:46:11 +0000
48@@ -2,13 +2,28 @@
49 PYTHON := /usr/bin/env python
50
51 lint:
52- @flake8 --exclude hooks/charmhelpers hooks
53- @flake8 --exclude hooks/charmhelpers unit_tests
54+ @flake8 --exclude hooks/charmhelpers,tests/charmhelpers \
55+ actions hooks unit_tests tests
56 @charm proof
57
58 test:
59+ @# Bundletester expects unit tests here
60 @echo Starting tests...
61- @$(PYTHON) /usr/bin/nosetests --nologcapture unit_tests
62-
63-sync:
64- @charm-helper-sync -c charm-helpers-sync.yaml
65+ @$(PYTHON) /usr/bin/nosetests -v --nologcapture --with-coverage unit_tests
66+
67+functional_test:
68+ @echo Starting Amulet tests...
69+ @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
70+
71+bin/charm_helpers_sync.py:
72+ @mkdir -p bin
73+ @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
74+ > bin/charm_helpers_sync.py
75+
76+sync: bin/charm_helpers_sync.py
77+ @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
78+ @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
79+
80+publish: lint test
81+ bzr push lp:charms/neutron-openvswitch
82+ bzr push lp:charms/trusty/neutron-openvswitch
83
84=== modified file 'README.md'
85--- README.md 2014-06-23 13:00:45 +0000
86+++ README.md 2015-11-12 11:46:11 +0000
87@@ -1,18 +1,134 @@
88-Overview
89---------
90-
91-This subordinate charm provides the Neutron OVS configuration for a compute
92-node. Oncde deployed it takes over the management of the neutron configuration
93-and plugin configuration on the compute node. It expects three relations:
94-
95-1) Relation with principle compute node
96-2) Relation with message broker. If a single message broker is being used for
97- the openstack deployemnt then it can relat to that. If a seperate neutron
98- message broker is being used it should relate to that.
99-3) Relation with neutron-api principle charm (not nova-cloud-controller)
100-
101-Restrictions:
102-------------
103-
104-It should only be used with Icehouse and above and requires a seperate
105-neutron-api service to have been deployed.
106+# Overview
107+
108+This subordinate charm provides the Neutron OpenvSwitch configuration for a compute node.
109+
110+Once deployed it takes over the management of the Neutron base and plugin configuration on the compute node.
111+
112+# Usage
113+
114+To deploy (partial deployment of linked charms only):
115+
116+ juju deploy rabbitmq-server
117+ juju deploy neutron-api
118+ juju deploy nova-compute
119+ juju deploy neutron-openvswitch
120+ juju add-relation neutron-openvswitch nova-compute
121+ juju add-relation neutron-openvswitch neutron-api
122+ juju add-relation neutron-openvswitch rabbitmq-server
123+
124+Note that the rabbitmq-server can optionally be a different instance of the rabbitmq-server charm than used by OpenStack Nova:
125+
126+ juju deploy rabbitmq-server rmq-neutron
127+ juju add-relation neutron-openvswitch rmq-neutron
128+ juju add-relation neutron-api rmq-neutron
129+
130+The neutron-api and neutron-openvswitch charms must be related to the same instance of the rabbitmq-server charm.
131+
132+# Restrictions
133+
134+It should only be used with OpenStack Icehouse and above and requires a seperate neutron-api service to have been deployed.
135+
136+# Disabling security group management
137+
138+WARNING: this feature allows you to effectively disable security on your cloud!
139+
140+This charm has a configuration option to allow users to disable any per-instance security group management; this must used with neutron-security-groups enabled in the neutron-api charm and could be used to turn off security on selected set of compute nodes:
141+
142+ juju deploy neutron-openvswitch neutron-openvswitch-insecure
143+ juju set neutron-openvswitch-insecure disable-security-groups=True
144+ juju deploy nova-compute nova-compute-insecure
145+ juju add-relation nova-compute-insecure neutron-openvswitch-insecure
146+ ...
147+
148+These compute nodes could then be accessed by cloud users via use of host aggregates with specific flavors to target instances to hypervisors with no per-instance security.
149+
150+# Deploying from source
151+
152+The minimum openstack-origin-git config required to deploy from source is:
153+
154+ openstack-origin-git: include-file://neutron-juno.yaml
155+
156+ neutron-juno.yaml
157+ repositories:
158+ - {name: requirements,
159+ repository: 'git://github.com/openstack/requirements',
160+ branch: stable/juno}
161+ - {name: neutron,
162+ repository: 'git://github.com/openstack/neutron',
163+ branch: stable/juno}
164+
165+Note that there are only two 'name' values the charm knows about: 'requirements'
166+and 'neutron'. These repositories must correspond to these 'name' values.
167+Additionally, the requirements repository must be specified first and the
168+neutron repository must be specified last. All other repostories are installed
169+in the order in which they are specified.
170+
171+The following is a full list of current tip repos (may not be up-to-date):
172+
173+ openstack-origin-git: include-file://neutron-master.yaml
174+
175+ neutron-master.yaml
176+ repositories:
177+ - {name: requirements,
178+ repository: 'git://github.com/openstack/requirements',
179+ branch: master}
180+ - {name: oslo-concurrency,
181+ repository: 'git://github.com/openstack/oslo.concurrency',
182+ branch: master}
183+ - {name: oslo-config,
184+ repository: 'git://github.com/openstack/oslo.config',
185+ branch: master}
186+ - {name: oslo-context,
187+ repository: 'git://github.com/openstack/oslo.context',
188+ branch: master}
189+ - {name: oslo-db,
190+ repository: 'git://github.com/openstack/oslo.db',
191+ branch: master}
192+ - {name: oslo-i18n,
193+ repository: 'git://github.com/openstack/oslo.i18n',
194+ branch: master}
195+ - {name: oslo-messaging,
196+ repository: 'git://github.com/openstack/oslo.messaging',
197+ branch: master}
198+ - {name: oslo-middleware,
199+ repository': 'git://github.com/openstack/oslo.middleware',
200+ branch: master}
201+ - {name: oslo-rootwrap',
202+ repository: 'git://github.com/openstack/oslo.rootwrap',
203+ branch: master}
204+ - {name: oslo-serialization,
205+ repository: 'git://github.com/openstack/oslo.serialization',
206+ branch: master}
207+ - {name: oslo-utils,
208+ repository: 'git://github.com/openstack/oslo.utils',
209+ branch: master}
210+ - {name: pbr,
211+ repository: 'git://github.com/openstack-dev/pbr',
212+ branch: master}
213+ - {name: stevedore,
214+ repository: 'git://github.com/openstack/stevedore',
215+ branch: 'master'}
216+ - {name: python-keystoneclient,
217+ repository: 'git://github.com/openstack/python-keystoneclient',
218+ branch: master}
219+ - {name: python-neutronclient,
220+ repository: 'git://github.com/openstack/python-neutronclient',
221+ branch: master}
222+ - {name: python-novaclient,
223+ repository': 'git://github.com/openstack/python-novaclient',
224+ branch: master}
225+ - {name: keystonemiddleware,
226+ repository: 'git://github.com/openstack/keystonemiddleware',
227+ branch: master}
228+ - {name: neutron-fwaas,
229+ repository': 'git://github.com/openstack/neutron-fwaas',
230+ branch: master}
231+ - {name: neutron-lbaas,
232+ repository: 'git://github.com/openstack/neutron-lbaas',
233+ branch: master}
234+ - {name: neutron-vpnaas,
235+ repository: 'git://github.com/openstack/neutron-vpnaas',
236+ branch: master}
237+ - {name: neutron,
238+ repository: 'git://github.com/openstack/neutron',
239+ branch: master}
240
241=== added directory 'actions'
242=== added file 'actions.yaml'
243--- actions.yaml 1970-01-01 00:00:00 +0000
244+++ actions.yaml 2015-11-12 11:46:11 +0000
245@@ -0,0 +1,2 @@
246+git-reinstall:
247+ description: Reinstall neutron-openvswitch from the openstack-origin-git repositories.
248
249=== added symlink 'actions/git-reinstall'
250=== target is u'git_reinstall.py'
251=== added file 'actions/git_reinstall.py'
252--- actions/git_reinstall.py 1970-01-01 00:00:00 +0000
253+++ actions/git_reinstall.py 2015-11-12 11:46:11 +0000
254@@ -0,0 +1,45 @@
255+#!/usr/bin/python
256+import sys
257+import traceback
258+
259+sys.path.append('hooks/')
260+
261+from charmhelpers.contrib.openstack.utils import (
262+ git_install_requested,
263+)
264+
265+from charmhelpers.core.hookenv import (
266+ action_set,
267+ action_fail,
268+ config,
269+)
270+
271+from neutron_ovs_utils import (
272+ git_install,
273+)
274+
275+from neutron_ovs_hooks import (
276+ config_changed,
277+)
278+
279+
280+def git_reinstall():
281+ """Reinstall from source and restart services.
282+
283+ If the openstack-origin-git config option was used to install openstack
284+ from source git repositories, then this action can be used to reinstall
285+ from updated git repositories, followed by a restart of services."""
286+ if not git_install_requested():
287+ action_fail('openstack-origin-git is not configured')
288+ return
289+
290+ try:
291+ git_install(config('openstack-origin-git'))
292+ config_changed()
293+ except:
294+ action_set({'traceback': traceback.format_exc()})
295+ action_fail('git-reinstall resulted in an unexpected error')
296+
297+
298+if __name__ == '__main__':
299+ git_reinstall()
300
301=== added file 'charm-helpers-hooks.yaml'
302--- charm-helpers-hooks.yaml 1970-01-01 00:00:00 +0000
303+++ charm-helpers-hooks.yaml 2015-11-12 11:46:11 +0000
304@@ -0,0 +1,13 @@
305+branch: lp:~openstack-charmers/charm-helpers/stable
306+destination: hooks/charmhelpers
307+include:
308+ - core
309+ - cli
310+ - fetch
311+ - contrib.openstack|inc=*
312+ - contrib.hahelpers
313+ - contrib.network.ovs
314+ - contrib.storage.linux
315+ - payload.execd
316+ - contrib.network.ip
317+ - contrib.python.packages
318
319=== removed file 'charm-helpers-sync.yaml'
320--- charm-helpers-sync.yaml 2014-06-19 09:56:25 +0000
321+++ charm-helpers-sync.yaml 1970-01-01 00:00:00 +0000
322@@ -1,10 +0,0 @@
323-branch: lp:charm-helpers
324-destination: hooks/charmhelpers
325-include:
326- - core
327- - fetch
328- - contrib.openstack
329- - contrib.hahelpers
330- - contrib.network.ovs
331- - contrib.storage.linux
332- - payload.execd
333
334=== added file 'charm-helpers-tests.yaml'
335--- charm-helpers-tests.yaml 1970-01-01 00:00:00 +0000
336+++ charm-helpers-tests.yaml 2015-11-12 11:46:11 +0000
337@@ -0,0 +1,5 @@
338+branch: lp:~openstack-charmers/charm-helpers/stable
339+destination: tests/charmhelpers
340+include:
341+ - contrib.amulet
342+ - contrib.openstack.amulet
343
344=== modified file 'config.yaml'
345--- config.yaml 2014-06-23 11:49:58 +0000
346+++ config.yaml 2015-11-12 11:46:11 +0000
347@@ -1,23 +1,108 @@
348 options:
349+ debug:
350+ default: False
351+ type: boolean
352+ description: Enable debug logging.
353+ verbose:
354+ default: False
355+ type: boolean
356+ description: Enable verbose logging.
357+ use-syslog:
358+ type: boolean
359+ default: False
360+ description: |
361+ Setting this to True will allow supporting services to log to syslog.
362+ openstack-origin-git:
363+ default:
364+ type: string
365+ description: |
366+ Specifies a YAML-formatted dictionary listing the git
367+ repositories and branches from which to install OpenStack and
368+ its dependencies.
369+
370+ When openstack-origin-git is specified, openstack-specific
371+ packages will be installed from source rather than from the
372+ the nova-compute charm's openstack-origin repository.
373+
374+ Note that the installed config files will be determined based on
375+ the OpenStack release of the nova-compute charm's openstack-origin
376+ option.
377+
378+ For more details see README.md.
379 rabbit-user:
380 default: neutron
381 type: string
382- description: Username used to access rabbitmq queue
383+ description: Username used to access RabbitMQ queue
384 rabbit-vhost:
385 default: openstack
386 type: string
387- description: Rabbitmq vhost
388- use-syslog:
389- type: boolean
390- default: False
391- description: |
392- By default, all services will log into their corresponding log files.
393- Setting this to True will force all services to log to the syslog.
394- debug:
395- default: False
396- type: boolean
397- description: Enable debug logging
398- verbose:
399- default: False
400- type: boolean
401- description: Enable verbose logging
402+ description: RabbitMQ vhost
403+ data-port:
404+ type: string
405+ default:
406+ description: |
407+ Space-delimited list of bridge:port mappings. Ports will be added to
408+ their corresponding bridge. The bridges will allow usage of flat or
409+ VLAN network types with Neutron and should match this defined in
410+ bridge-mappings.
411+ .
412+ Ports provided can be the name or MAC address of the interface to be
413+ added to the bridge. If MAC addresses are used, you may provide multiple
414+ bridge:mac for the same bridge so as to be able to configure multiple
415+ units. In this case the charm will run through the provided MAC addresses
416+ for each bridge until it finds one it can resolve to an interface name.
417+ disable-security-groups:
418+ type: boolean
419+ default: false
420+ description: |
421+ Disable neutron based security groups - setting this configuration option
422+ will override any settings configured via the neutron-api charm.
423+ .
424+ BE CAREFUL - this option allows you to disable all port level security
425+ within an OpenStack cloud.
426+ bridge-mappings:
427+ type: string
428+ default: 'physnet1:br-data'
429+ description: |
430+ Space-delimited list of ML2 data bridge mappings with format
431+ <provider>:<bridge>.
432+ flat-network-providers:
433+ type: string
434+ default:
435+ description: |
436+ Space-delimited list of Neutron flat network providers.
437+ vlan-ranges:
438+ type: string
439+ default: "physnet1:1000:2000"
440+ description: |
441+ Space-delimited list of <physical_network>:<vlan_min>:<vlan_max> or
442+ <physical_network> specifying physical_network names usable for VLAN
443+ provider and tenant networks, as well as ranges of VLAN tags on each
444+ available for allocation to tenant networks.
445+ # Network configuration options
446+ # by default all access is over 'private-address'
447+ os-data-network:
448+ type: string
449+ default:
450+ description: |
451+ The IP address and netmask of the OpenStack Data network (e.g.,
452+ 192.168.0.0/24)
453+ .
454+ This network will be used for tenant network traffic in overlay
455+ networks.
456+ ext-port:
457+ type: string
458+ default:
459+ description: |
460+ A space-separated list of external ports to use for routing of instance
461+ traffic to the external public network. Valid values are either MAC
462+ addresses (in which case only MAC addresses for interfaces without an IP
463+ address already assigned will be used), or interfaces (eth0)
464+ enable-local-dhcp-and-metadata:
465+ type: boolean
466+ default: false
467+ description: |
468+ Enable local Neutron DHCP and Metadata Agents. This is useful for deployments
469+ which do not include a neutron-gateway (do not require l3, lbaas or vpnaas
470+ services) and should only be used in-conjunction with flat or VLAN provider
471+ networks configurations.
472
473=== added file 'hooks/charmhelpers/__init__.py'
474--- hooks/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
475+++ hooks/charmhelpers/__init__.py 2015-11-12 11:46:11 +0000
476@@ -0,0 +1,38 @@
477+# Copyright 2014-2015 Canonical Limited.
478+#
479+# This file is part of charm-helpers.
480+#
481+# charm-helpers is free software: you can redistribute it and/or modify
482+# it under the terms of the GNU Lesser General Public License version 3 as
483+# published by the Free Software Foundation.
484+#
485+# charm-helpers is distributed in the hope that it will be useful,
486+# but WITHOUT ANY WARRANTY; without even the implied warranty of
487+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
488+# GNU Lesser General Public License for more details.
489+#
490+# You should have received a copy of the GNU Lesser General Public License
491+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
492+
493+# Bootstrap charm-helpers, installing its dependencies if necessary using
494+# only standard libraries.
495+import subprocess
496+import sys
497+
498+try:
499+ import six # flake8: noqa
500+except ImportError:
501+ if sys.version_info.major == 2:
502+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
503+ else:
504+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
505+ import six # flake8: noqa
506+
507+try:
508+ import yaml # flake8: noqa
509+except ImportError:
510+ if sys.version_info.major == 2:
511+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
512+ else:
513+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
514+ import yaml # flake8: noqa
515
516=== removed file 'hooks/charmhelpers/__init__.py'
517=== added directory 'hooks/charmhelpers/cli'
518=== added file 'hooks/charmhelpers/cli/__init__.py'
519--- hooks/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
520+++ hooks/charmhelpers/cli/__init__.py 2015-11-12 11:46:11 +0000
521@@ -0,0 +1,191 @@
522+# Copyright 2014-2015 Canonical Limited.
523+#
524+# This file is part of charm-helpers.
525+#
526+# charm-helpers is free software: you can redistribute it and/or modify
527+# it under the terms of the GNU Lesser General Public License version 3 as
528+# published by the Free Software Foundation.
529+#
530+# charm-helpers is distributed in the hope that it will be useful,
531+# but WITHOUT ANY WARRANTY; without even the implied warranty of
532+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
533+# GNU Lesser General Public License for more details.
534+#
535+# You should have received a copy of the GNU Lesser General Public License
536+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
537+
538+import inspect
539+import argparse
540+import sys
541+
542+from six.moves import zip
543+
544+from charmhelpers.core import unitdata
545+
546+
547+class OutputFormatter(object):
548+ def __init__(self, outfile=sys.stdout):
549+ self.formats = (
550+ "raw",
551+ "json",
552+ "py",
553+ "yaml",
554+ "csv",
555+ "tab",
556+ )
557+ self.outfile = outfile
558+
559+ def add_arguments(self, argument_parser):
560+ formatgroup = argument_parser.add_mutually_exclusive_group()
561+ choices = self.supported_formats
562+ formatgroup.add_argument("--format", metavar='FMT',
563+ help="Select output format for returned data, "
564+ "where FMT is one of: {}".format(choices),
565+ choices=choices, default='raw')
566+ for fmt in self.formats:
567+ fmtfunc = getattr(self, fmt)
568+ formatgroup.add_argument("-{}".format(fmt[0]),
569+ "--{}".format(fmt), action='store_const',
570+ const=fmt, dest='format',
571+ help=fmtfunc.__doc__)
572+
573+ @property
574+ def supported_formats(self):
575+ return self.formats
576+
577+ def raw(self, output):
578+ """Output data as raw string (default)"""
579+ if isinstance(output, (list, tuple)):
580+ output = '\n'.join(map(str, output))
581+ self.outfile.write(str(output))
582+
583+ def py(self, output):
584+ """Output data as a nicely-formatted python data structure"""
585+ import pprint
586+ pprint.pprint(output, stream=self.outfile)
587+
588+ def json(self, output):
589+ """Output data in JSON format"""
590+ import json
591+ json.dump(output, self.outfile)
592+
593+ def yaml(self, output):
594+ """Output data in YAML format"""
595+ import yaml
596+ yaml.safe_dump(output, self.outfile)
597+
598+ def csv(self, output):
599+ """Output data as excel-compatible CSV"""
600+ import csv
601+ csvwriter = csv.writer(self.outfile)
602+ csvwriter.writerows(output)
603+
604+ def tab(self, output):
605+ """Output data in excel-compatible tab-delimited format"""
606+ import csv
607+ csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
608+ csvwriter.writerows(output)
609+
610+ def format_output(self, output, fmt='raw'):
611+ fmtfunc = getattr(self, fmt)
612+ fmtfunc(output)
613+
614+
615+class CommandLine(object):
616+ argument_parser = None
617+ subparsers = None
618+ formatter = None
619+ exit_code = 0
620+
621+ def __init__(self):
622+ if not self.argument_parser:
623+ self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
624+ if not self.formatter:
625+ self.formatter = OutputFormatter()
626+ self.formatter.add_arguments(self.argument_parser)
627+ if not self.subparsers:
628+ self.subparsers = self.argument_parser.add_subparsers(help='Commands')
629+
630+ def subcommand(self, command_name=None):
631+ """
632+ Decorate a function as a subcommand. Use its arguments as the
633+ command-line arguments"""
634+ def wrapper(decorated):
635+ cmd_name = command_name or decorated.__name__
636+ subparser = self.subparsers.add_parser(cmd_name,
637+ description=decorated.__doc__)
638+ for args, kwargs in describe_arguments(decorated):
639+ subparser.add_argument(*args, **kwargs)
640+ subparser.set_defaults(func=decorated)
641+ return decorated
642+ return wrapper
643+
644+ def test_command(self, decorated):
645+ """
646+ Subcommand is a boolean test function, so bool return values should be
647+ converted to a 0/1 exit code.
648+ """
649+ decorated._cli_test_command = True
650+ return decorated
651+
652+ def no_output(self, decorated):
653+ """
654+ Subcommand is not expected to return a value, so don't print a spurious None.
655+ """
656+ decorated._cli_no_output = True
657+ return decorated
658+
659+ def subcommand_builder(self, command_name, description=None):
660+ """
661+ Decorate a function that builds a subcommand. Builders should accept a
662+ single argument (the subparser instance) and return the function to be
663+ run as the command."""
664+ def wrapper(decorated):
665+ subparser = self.subparsers.add_parser(command_name)
666+ func = decorated(subparser)
667+ subparser.set_defaults(func=func)
668+ subparser.description = description or func.__doc__
669+ return wrapper
670+
671+ def run(self):
672+ "Run cli, processing arguments and executing subcommands."
673+ arguments = self.argument_parser.parse_args()
674+ argspec = inspect.getargspec(arguments.func)
675+ vargs = []
676+ for arg in argspec.args:
677+ vargs.append(getattr(arguments, arg))
678+ if argspec.varargs:
679+ vargs.extend(getattr(arguments, argspec.varargs))
680+ output = arguments.func(*vargs)
681+ if getattr(arguments.func, '_cli_test_command', False):
682+ self.exit_code = 0 if output else 1
683+ output = ''
684+ if getattr(arguments.func, '_cli_no_output', False):
685+ output = ''
686+ self.formatter.format_output(output, arguments.format)
687+ if unitdata._KV:
688+ unitdata._KV.flush()
689+
690+
691+cmdline = CommandLine()
692+
693+
694+def describe_arguments(func):
695+ """
696+ Analyze a function's signature and return a data structure suitable for
697+ passing in as arguments to an argparse parser's add_argument() method."""
698+
699+ argspec = inspect.getargspec(func)
700+ # we should probably raise an exception somewhere if func includes **kwargs
701+ if argspec.defaults:
702+ positional_args = argspec.args[:-len(argspec.defaults)]
703+ keyword_names = argspec.args[-len(argspec.defaults):]
704+ for arg, default in zip(keyword_names, argspec.defaults):
705+ yield ('--{}'.format(arg),), {'default': default}
706+ else:
707+ positional_args = argspec.args
708+
709+ for arg in positional_args:
710+ yield (arg,), {}
711+ if argspec.varargs:
712+ yield (argspec.varargs,), {'nargs': '*'}
713
714=== added file 'hooks/charmhelpers/cli/benchmark.py'
715--- hooks/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
716+++ hooks/charmhelpers/cli/benchmark.py 2015-11-12 11:46:11 +0000
717@@ -0,0 +1,36 @@
718+# Copyright 2014-2015 Canonical Limited.
719+#
720+# This file is part of charm-helpers.
721+#
722+# charm-helpers is free software: you can redistribute it and/or modify
723+# it under the terms of the GNU Lesser General Public License version 3 as
724+# published by the Free Software Foundation.
725+#
726+# charm-helpers is distributed in the hope that it will be useful,
727+# but WITHOUT ANY WARRANTY; without even the implied warranty of
728+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
729+# GNU Lesser General Public License for more details.
730+#
731+# You should have received a copy of the GNU Lesser General Public License
732+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
733+
734+from . import cmdline
735+from charmhelpers.contrib.benchmark import Benchmark
736+
737+
738+@cmdline.subcommand(command_name='benchmark-start')
739+def start():
740+ Benchmark.start()
741+
742+
743+@cmdline.subcommand(command_name='benchmark-finish')
744+def finish():
745+ Benchmark.finish()
746+
747+
748+@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
749+def service(subparser):
750+ subparser.add_argument("value", help="The composite score.")
751+ subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
752+ subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
753+ return Benchmark.set_composite_score
754
755=== added file 'hooks/charmhelpers/cli/commands.py'
756--- hooks/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
757+++ hooks/charmhelpers/cli/commands.py 2015-11-12 11:46:11 +0000
758@@ -0,0 +1,32 @@
759+# Copyright 2014-2015 Canonical Limited.
760+#
761+# This file is part of charm-helpers.
762+#
763+# charm-helpers is free software: you can redistribute it and/or modify
764+# it under the terms of the GNU Lesser General Public License version 3 as
765+# published by the Free Software Foundation.
766+#
767+# charm-helpers is distributed in the hope that it will be useful,
768+# but WITHOUT ANY WARRANTY; without even the implied warranty of
769+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
770+# GNU Lesser General Public License for more details.
771+#
772+# You should have received a copy of the GNU Lesser General Public License
773+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
774+
775+"""
776+This module loads sub-modules into the python runtime so they can be
777+discovered via the inspect module. In order to prevent flake8 from (rightfully)
778+telling us these are unused modules, throw a ' # noqa' at the end of each import
779+so that the warning is suppressed.
780+"""
781+
782+from . import CommandLine # noqa
783+
784+"""
785+Import the sub-modules which have decorated subcommands to register with chlp.
786+"""
787+from . import host # noqa
788+from . import benchmark # noqa
789+from . import unitdata # noqa
790+from . import hookenv # noqa
791
792=== added file 'hooks/charmhelpers/cli/hookenv.py'
793--- hooks/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
794+++ hooks/charmhelpers/cli/hookenv.py 2015-11-12 11:46:11 +0000
795@@ -0,0 +1,23 @@
796+# Copyright 2014-2015 Canonical Limited.
797+#
798+# This file is part of charm-helpers.
799+#
800+# charm-helpers is free software: you can redistribute it and/or modify
801+# it under the terms of the GNU Lesser General Public License version 3 as
802+# published by the Free Software Foundation.
803+#
804+# charm-helpers is distributed in the hope that it will be useful,
805+# but WITHOUT ANY WARRANTY; without even the implied warranty of
806+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
807+# GNU Lesser General Public License for more details.
808+#
809+# You should have received a copy of the GNU Lesser General Public License
810+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
811+
812+from . import cmdline
813+from charmhelpers.core import hookenv
814+
815+
816+cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
817+cmdline.subcommand('service-name')(hookenv.service_name)
818+cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
819
820=== added file 'hooks/charmhelpers/cli/host.py'
821--- hooks/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
822+++ hooks/charmhelpers/cli/host.py 2015-11-12 11:46:11 +0000
823@@ -0,0 +1,31 @@
824+# Copyright 2014-2015 Canonical Limited.
825+#
826+# This file is part of charm-helpers.
827+#
828+# charm-helpers is free software: you can redistribute it and/or modify
829+# it under the terms of the GNU Lesser General Public License version 3 as
830+# published by the Free Software Foundation.
831+#
832+# charm-helpers is distributed in the hope that it will be useful,
833+# but WITHOUT ANY WARRANTY; without even the implied warranty of
834+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
835+# GNU Lesser General Public License for more details.
836+#
837+# You should have received a copy of the GNU Lesser General Public License
838+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
839+
840+from . import cmdline
841+from charmhelpers.core import host
842+
843+
844+@cmdline.subcommand()
845+def mounts():
846+ "List mounts"
847+ return host.mounts()
848+
849+
850+@cmdline.subcommand_builder('service', description="Control system services")
851+def service(subparser):
852+ subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
853+ subparser.add_argument("service_name", help="Name of the service to control")
854+ return host.service
855
856=== added file 'hooks/charmhelpers/cli/unitdata.py'
857--- hooks/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
858+++ hooks/charmhelpers/cli/unitdata.py 2015-11-12 11:46:11 +0000
859@@ -0,0 +1,39 @@
860+# Copyright 2014-2015 Canonical Limited.
861+#
862+# This file is part of charm-helpers.
863+#
864+# charm-helpers is free software: you can redistribute it and/or modify
865+# it under the terms of the GNU Lesser General Public License version 3 as
866+# published by the Free Software Foundation.
867+#
868+# charm-helpers is distributed in the hope that it will be useful,
869+# but WITHOUT ANY WARRANTY; without even the implied warranty of
870+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
871+# GNU Lesser General Public License for more details.
872+#
873+# You should have received a copy of the GNU Lesser General Public License
874+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
875+
876+from . import cmdline
877+from charmhelpers.core import unitdata
878+
879+
880+@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
881+def unitdata_cmd(subparser):
882+ nested = subparser.add_subparsers()
883+ get_cmd = nested.add_parser('get', help='Retrieve data')
884+ get_cmd.add_argument('key', help='Key to retrieve the value of')
885+ get_cmd.set_defaults(action='get', value=None)
886+ set_cmd = nested.add_parser('set', help='Store data')
887+ set_cmd.add_argument('key', help='Key to set')
888+ set_cmd.add_argument('value', help='Value to store')
889+ set_cmd.set_defaults(action='set')
890+
891+ def _unitdata_cmd(action, key, value):
892+ if action == 'get':
893+ return unitdata.kv().get(key)
894+ elif action == 'set':
895+ unitdata.kv().set(key, value)
896+ unitdata.kv().flush()
897+ return ''
898+ return _unitdata_cmd
899
900=== modified file 'hooks/charmhelpers/contrib/__init__.py'
901--- hooks/charmhelpers/contrib/__init__.py 2014-06-05 10:59:23 +0000
902+++ hooks/charmhelpers/contrib/__init__.py 2015-11-12 11:46:11 +0000
903@@ -0,0 +1,15 @@
904+# Copyright 2014-2015 Canonical Limited.
905+#
906+# This file is part of charm-helpers.
907+#
908+# charm-helpers is free software: you can redistribute it and/or modify
909+# it under the terms of the GNU Lesser General Public License version 3 as
910+# published by the Free Software Foundation.
911+#
912+# charm-helpers is distributed in the hope that it will be useful,
913+# but WITHOUT ANY WARRANTY; without even the implied warranty of
914+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
915+# GNU Lesser General Public License for more details.
916+#
917+# You should have received a copy of the GNU Lesser General Public License
918+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
919
920=== modified file 'hooks/charmhelpers/contrib/hahelpers/__init__.py'
921--- hooks/charmhelpers/contrib/hahelpers/__init__.py 2014-06-05 10:59:23 +0000
922+++ hooks/charmhelpers/contrib/hahelpers/__init__.py 2015-11-12 11:46:11 +0000
923@@ -0,0 +1,15 @@
924+# Copyright 2014-2015 Canonical Limited.
925+#
926+# This file is part of charm-helpers.
927+#
928+# charm-helpers is free software: you can redistribute it and/or modify
929+# it under the terms of the GNU Lesser General Public License version 3 as
930+# published by the Free Software Foundation.
931+#
932+# charm-helpers is distributed in the hope that it will be useful,
933+# but WITHOUT ANY WARRANTY; without even the implied warranty of
934+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
935+# GNU Lesser General Public License for more details.
936+#
937+# You should have received a copy of the GNU Lesser General Public License
938+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
939
940=== modified file 'hooks/charmhelpers/contrib/hahelpers/apache.py'
941--- hooks/charmhelpers/contrib/hahelpers/apache.py 2014-06-05 10:59:23 +0000
942+++ hooks/charmhelpers/contrib/hahelpers/apache.py 2015-11-12 11:46:11 +0000
943@@ -1,3 +1,19 @@
944+# Copyright 2014-2015 Canonical Limited.
945+#
946+# This file is part of charm-helpers.
947+#
948+# charm-helpers is free software: you can redistribute it and/or modify
949+# it under the terms of the GNU Lesser General Public License version 3 as
950+# published by the Free Software Foundation.
951+#
952+# charm-helpers is distributed in the hope that it will be useful,
953+# but WITHOUT ANY WARRANTY; without even the implied warranty of
954+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
955+# GNU Lesser General Public License for more details.
956+#
957+# You should have received a copy of the GNU Lesser General Public License
958+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
959+
960 #
961 # Copyright 2012 Canonical Ltd.
962 #
963@@ -20,20 +36,27 @@
964 )
965
966
967-def get_cert():
968+def get_cert(cn=None):
969+ # TODO: deal with multiple https endpoints via charm config
970 cert = config_get('ssl_cert')
971 key = config_get('ssl_key')
972 if not (cert and key):
973 log("Inspecting identity-service relations for SSL certificate.",
974 level=INFO)
975 cert = key = None
976+ if cn:
977+ ssl_cert_attr = 'ssl_cert_{}'.format(cn)
978+ ssl_key_attr = 'ssl_key_{}'.format(cn)
979+ else:
980+ ssl_cert_attr = 'ssl_cert'
981+ ssl_key_attr = 'ssl_key'
982 for r_id in relation_ids('identity-service'):
983 for unit in relation_list(r_id):
984 if not cert:
985- cert = relation_get('ssl_cert',
986+ cert = relation_get(ssl_cert_attr,
987 rid=r_id, unit=unit)
988 if not key:
989- key = relation_get('ssl_key',
990+ key = relation_get(ssl_key_attr,
991 rid=r_id, unit=unit)
992 return (cert, key)
993
994
995=== removed file 'hooks/charmhelpers/contrib/hahelpers/ceph.py'
996--- hooks/charmhelpers/contrib/hahelpers/ceph.py 2014-06-05 10:59:23 +0000
997+++ hooks/charmhelpers/contrib/hahelpers/ceph.py 1970-01-01 00:00:00 +0000
998@@ -1,297 +0,0 @@
999-#
1000-# Copyright 2012 Canonical Ltd.
1001-#
1002-# This file is sourced from lp:openstack-charm-helpers
1003-#
1004-# Authors:
1005-# James Page <james.page@ubuntu.com>
1006-# Adam Gandelman <adamg@ubuntu.com>
1007-#
1008-
1009-import commands
1010-import os
1011-import shutil
1012-import time
1013-
1014-from subprocess import (
1015- check_call,
1016- check_output,
1017- CalledProcessError
1018-)
1019-
1020-from charmhelpers.core.hookenv import (
1021- relation_get,
1022- relation_ids,
1023- related_units,
1024- log,
1025- INFO,
1026- ERROR
1027-)
1028-
1029-from charmhelpers.fetch import (
1030- apt_install,
1031-)
1032-
1033-from charmhelpers.core.host import (
1034- mount,
1035- mounts,
1036- service_start,
1037- service_stop,
1038- umount,
1039-)
1040-
1041-KEYRING = '/etc/ceph/ceph.client.%s.keyring'
1042-KEYFILE = '/etc/ceph/ceph.client.%s.key'
1043-
1044-CEPH_CONF = """[global]
1045- auth supported = %(auth)s
1046- keyring = %(keyring)s
1047- mon host = %(mon_hosts)s
1048- log to syslog = %(use_syslog)s
1049- err to syslog = %(use_syslog)s
1050- clog to syslog = %(use_syslog)s
1051-"""
1052-
1053-
1054-def running(service):
1055- # this local util can be dropped as soon the following branch lands
1056- # in lp:charm-helpers
1057- # https://code.launchpad.net/~gandelman-a/charm-helpers/service_running/
1058- try:
1059- output = check_output(['service', service, 'status'])
1060- except CalledProcessError:
1061- return False
1062- else:
1063- if ("start/running" in output or "is running" in output):
1064- return True
1065- else:
1066- return False
1067-
1068-
1069-def install():
1070- ceph_dir = "/etc/ceph"
1071- if not os.path.isdir(ceph_dir):
1072- os.mkdir(ceph_dir)
1073- apt_install('ceph-common', fatal=True)
1074-
1075-
1076-def rbd_exists(service, pool, rbd_img):
1077- (rc, out) = commands.getstatusoutput('rbd list --id %s --pool %s' %
1078- (service, pool))
1079- return rbd_img in out
1080-
1081-
1082-def create_rbd_image(service, pool, image, sizemb):
1083- cmd = [
1084- 'rbd',
1085- 'create',
1086- image,
1087- '--size',
1088- str(sizemb),
1089- '--id',
1090- service,
1091- '--pool',
1092- pool
1093- ]
1094- check_call(cmd)
1095-
1096-
1097-def pool_exists(service, name):
1098- (rc, out) = commands.getstatusoutput("rados --id %s lspools" % service)
1099- return name in out
1100-
1101-
1102-def create_pool(service, name):
1103- cmd = [
1104- 'rados',
1105- '--id',
1106- service,
1107- 'mkpool',
1108- name
1109- ]
1110- check_call(cmd)
1111-
1112-
1113-def keyfile_path(service):
1114- return KEYFILE % service
1115-
1116-
1117-def keyring_path(service):
1118- return KEYRING % service
1119-
1120-
1121-def create_keyring(service, key):
1122- keyring = keyring_path(service)
1123- if os.path.exists(keyring):
1124- log('ceph: Keyring exists at %s.' % keyring, level=INFO)
1125- cmd = [
1126- 'ceph-authtool',
1127- keyring,
1128- '--create-keyring',
1129- '--name=client.%s' % service,
1130- '--add-key=%s' % key
1131- ]
1132- check_call(cmd)
1133- log('ceph: Created new ring at %s.' % keyring, level=INFO)
1134-
1135-
1136-def create_key_file(service, key):
1137- # create a file containing the key
1138- keyfile = keyfile_path(service)
1139- if os.path.exists(keyfile):
1140- log('ceph: Keyfile exists at %s.' % keyfile, level=INFO)
1141- fd = open(keyfile, 'w')
1142- fd.write(key)
1143- fd.close()
1144- log('ceph: Created new keyfile at %s.' % keyfile, level=INFO)
1145-
1146-
1147-def get_ceph_nodes():
1148- hosts = []
1149- for r_id in relation_ids('ceph'):
1150- for unit in related_units(r_id):
1151- hosts.append(relation_get('private-address', unit=unit, rid=r_id))
1152- return hosts
1153-
1154-
1155-def configure(service, key, auth):
1156- create_keyring(service, key)
1157- create_key_file(service, key)
1158- hosts = get_ceph_nodes()
1159- mon_hosts = ",".join(map(str, hosts))
1160- keyring = keyring_path(service)
1161- with open('/etc/ceph/ceph.conf', 'w') as ceph_conf:
1162- ceph_conf.write(CEPH_CONF % locals())
1163- modprobe_kernel_module('rbd')
1164-
1165-
1166-def image_mapped(image_name):
1167- (rc, out) = commands.getstatusoutput('rbd showmapped')
1168- return image_name in out
1169-
1170-
1171-def map_block_storage(service, pool, image):
1172- cmd = [
1173- 'rbd',
1174- 'map',
1175- '%s/%s' % (pool, image),
1176- '--user',
1177- service,
1178- '--secret',
1179- keyfile_path(service),
1180- ]
1181- check_call(cmd)
1182-
1183-
1184-def filesystem_mounted(fs):
1185- return fs in [f for m, f in mounts()]
1186-
1187-
1188-def make_filesystem(blk_device, fstype='ext4', timeout=10):
1189- count = 0
1190- e_noent = os.errno.ENOENT
1191- while not os.path.exists(blk_device):
1192- if count >= timeout:
1193- log('ceph: gave up waiting on block device %s' % blk_device,
1194- level=ERROR)
1195- raise IOError(e_noent, os.strerror(e_noent), blk_device)
1196- log('ceph: waiting for block device %s to appear' % blk_device,
1197- level=INFO)
1198- count += 1
1199- time.sleep(1)
1200- else:
1201- log('ceph: Formatting block device %s as filesystem %s.' %
1202- (blk_device, fstype), level=INFO)
1203- check_call(['mkfs', '-t', fstype, blk_device])
1204-
1205-
1206-def place_data_on_ceph(service, blk_device, data_src_dst, fstype='ext4'):
1207- # mount block device into /mnt
1208- mount(blk_device, '/mnt')
1209-
1210- # copy data to /mnt
1211- try:
1212- copy_files(data_src_dst, '/mnt')
1213- except:
1214- pass
1215-
1216- # umount block device
1217- umount('/mnt')
1218-
1219- _dir = os.stat(data_src_dst)
1220- uid = _dir.st_uid
1221- gid = _dir.st_gid
1222-
1223- # re-mount where the data should originally be
1224- mount(blk_device, data_src_dst, persist=True)
1225-
1226- # ensure original ownership of new mount.
1227- cmd = ['chown', '-R', '%s:%s' % (uid, gid), data_src_dst]
1228- check_call(cmd)
1229-
1230-
1231-# TODO: re-use
1232-def modprobe_kernel_module(module):
1233- log('ceph: Loading kernel module', level=INFO)
1234- cmd = ['modprobe', module]
1235- check_call(cmd)
1236- cmd = 'echo %s >> /etc/modules' % module
1237- check_call(cmd, shell=True)
1238-
1239-
1240-def copy_files(src, dst, symlinks=False, ignore=None):
1241- for item in os.listdir(src):
1242- s = os.path.join(src, item)
1243- d = os.path.join(dst, item)
1244- if os.path.isdir(s):
1245- shutil.copytree(s, d, symlinks, ignore)
1246- else:
1247- shutil.copy2(s, d)
1248-
1249-
1250-def ensure_ceph_storage(service, pool, rbd_img, sizemb, mount_point,
1251- blk_device, fstype, system_services=[]):
1252- """
1253- To be called from the current cluster leader.
1254- Ensures given pool and RBD image exists, is mapped to a block device,
1255- and the device is formatted and mounted at the given mount_point.
1256-
1257- If formatting a device for the first time, data existing at mount_point
1258- will be migrated to the RBD device before being remounted.
1259-
1260- All services listed in system_services will be stopped prior to data
1261- migration and restarted when complete.
1262- """
1263- # Ensure pool, RBD image, RBD mappings are in place.
1264- if not pool_exists(service, pool):
1265- log('ceph: Creating new pool %s.' % pool, level=INFO)
1266- create_pool(service, pool)
1267-
1268- if not rbd_exists(service, pool, rbd_img):
1269- log('ceph: Creating RBD image (%s).' % rbd_img, level=INFO)
1270- create_rbd_image(service, pool, rbd_img, sizemb)
1271-
1272- if not image_mapped(rbd_img):
1273- log('ceph: Mapping RBD Image as a Block Device.', level=INFO)
1274- map_block_storage(service, pool, rbd_img)
1275-
1276- # make file system
1277- # TODO: What happens if for whatever reason this is run again and
1278- # the data is already in the rbd device and/or is mounted??
1279- # When it is mounted already, it will fail to make the fs
1280- # XXX: This is really sketchy! Need to at least add an fstab entry
1281- # otherwise this hook will blow away existing data if its executed
1282- # after a reboot.
1283- if not filesystem_mounted(mount_point):
1284- make_filesystem(blk_device, fstype)
1285-
1286- for svc in system_services:
1287- if running(svc):
1288- log('Stopping services %s prior to migrating data.' % svc,
1289- level=INFO)
1290- service_stop(svc)
1291-
1292- place_data_on_ceph(service, blk_device, mount_point, fstype)
1293-
1294- for svc in system_services:
1295- service_start(svc)
1296
1297=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
1298--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-06-05 10:59:23 +0000
1299+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-11-12 11:46:11 +0000
1300@@ -1,3 +1,19 @@
1301+# Copyright 2014-2015 Canonical Limited.
1302+#
1303+# This file is part of charm-helpers.
1304+#
1305+# charm-helpers is free software: you can redistribute it and/or modify
1306+# it under the terms of the GNU Lesser General Public License version 3 as
1307+# published by the Free Software Foundation.
1308+#
1309+# charm-helpers is distributed in the hope that it will be useful,
1310+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1311+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1312+# GNU Lesser General Public License for more details.
1313+#
1314+# You should have received a copy of the GNU Lesser General Public License
1315+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1316+
1317 #
1318 # Copyright 2012 Canonical Ltd.
1319 #
1320@@ -6,11 +22,18 @@
1321 # Adam Gandelman <adamg@ubuntu.com>
1322 #
1323
1324+"""
1325+Helpers for clustering and determining "cluster leadership" and other
1326+clustering-related helpers.
1327+"""
1328+
1329 import subprocess
1330 import os
1331
1332 from socket import gethostname as get_unit_hostname
1333
1334+import six
1335+
1336 from charmhelpers.core.hookenv import (
1337 log,
1338 relation_ids,
1339@@ -19,14 +42,64 @@
1340 config as config_get,
1341 INFO,
1342 ERROR,
1343+ WARNING,
1344 unit_get,
1345-)
1346+ is_leader as juju_is_leader
1347+)
1348+from charmhelpers.core.decorators import (
1349+ retry_on_exception,
1350+)
1351+from charmhelpers.core.strutils import (
1352+ bool_from_string,
1353+)
1354+
1355+DC_RESOURCE_NAME = 'DC'
1356
1357
1358 class HAIncompleteConfig(Exception):
1359 pass
1360
1361
1362+class CRMResourceNotFound(Exception):
1363+ pass
1364+
1365+
1366+class CRMDCNotFound(Exception):
1367+ pass
1368+
1369+
1370+def is_elected_leader(resource):
1371+ """
1372+ Returns True if the charm executing this is the elected cluster leader.
1373+
1374+ It relies on two mechanisms to determine leadership:
1375+ 1. If juju is sufficiently new and leadership election is supported,
1376+ the is_leader command will be used.
1377+ 2. If the charm is part of a corosync cluster, call corosync to
1378+ determine leadership.
1379+ 3. If the charm is not part of a corosync cluster, the leader is
1380+ determined as being "the alive unit with the lowest unit numer". In
1381+ other words, the oldest surviving unit.
1382+ """
1383+ try:
1384+ return juju_is_leader()
1385+ except NotImplementedError:
1386+ log('Juju leadership election feature not enabled'
1387+ ', using fallback support',
1388+ level=WARNING)
1389+
1390+ if is_clustered():
1391+ if not is_crm_leader(resource):
1392+ log('Deferring action to CRM leader.', level=INFO)
1393+ return False
1394+ else:
1395+ peers = peer_units()
1396+ if peers and not oldest_peer(peers):
1397+ log('Deferring action to oldest service unit.', level=INFO)
1398+ return False
1399+ return True
1400+
1401+
1402 def is_clustered():
1403 for r_id in (relation_ids('ha') or []):
1404 for unit in (relation_list(r_id) or []):
1405@@ -38,31 +111,85 @@
1406 return False
1407
1408
1409+def is_crm_dc():
1410+ """
1411+ Determine leadership by querying the pacemaker Designated Controller
1412+ """
1413+ cmd = ['crm', 'status']
1414+ try:
1415+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
1416+ if not isinstance(status, six.text_type):
1417+ status = six.text_type(status, "utf-8")
1418+ except subprocess.CalledProcessError as ex:
1419+ raise CRMDCNotFound(str(ex))
1420+
1421+ current_dc = ''
1422+ for line in status.split('\n'):
1423+ if line.startswith('Current DC'):
1424+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
1425+ current_dc = line.split(':')[1].split()[0]
1426+ if current_dc == get_unit_hostname():
1427+ return True
1428+ elif current_dc == 'NONE':
1429+ raise CRMDCNotFound('Current DC: NONE')
1430+
1431+ return False
1432+
1433+
1434+@retry_on_exception(5, base_delay=2,
1435+ exc_type=(CRMResourceNotFound, CRMDCNotFound))
1436+def is_crm_leader(resource, retry=False):
1437+ """
1438+ Returns True if the charm calling this is the elected corosync leader,
1439+ as returned by calling the external "crm" command.
1440+
1441+ We allow this operation to be retried to avoid the possibility of getting a
1442+ false negative. See LP #1396246 for more info.
1443+ """
1444+ if resource == DC_RESOURCE_NAME:
1445+ return is_crm_dc()
1446+ cmd = ['crm', 'resource', 'show', resource]
1447+ try:
1448+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
1449+ if not isinstance(status, six.text_type):
1450+ status = six.text_type(status, "utf-8")
1451+ except subprocess.CalledProcessError:
1452+ status = None
1453+
1454+ if status and get_unit_hostname() in status:
1455+ return True
1456+
1457+ if status and "resource %s is NOT running" % (resource) in status:
1458+ raise CRMResourceNotFound("CRM resource %s not found" % (resource))
1459+
1460+ return False
1461+
1462+
1463 def is_leader(resource):
1464- cmd = [
1465- "crm", "resource",
1466- "show", resource
1467- ]
1468- try:
1469- status = subprocess.check_output(cmd)
1470- except subprocess.CalledProcessError:
1471- return False
1472- else:
1473- if get_unit_hostname() in status:
1474- return True
1475- else:
1476- return False
1477-
1478-
1479-def peer_units():
1480+ log("is_leader is deprecated. Please consider using is_crm_leader "
1481+ "instead.", level=WARNING)
1482+ return is_crm_leader(resource)
1483+
1484+
1485+def peer_units(peer_relation="cluster"):
1486 peers = []
1487- for r_id in (relation_ids('cluster') or []):
1488+ for r_id in (relation_ids(peer_relation) or []):
1489 for unit in (relation_list(r_id) or []):
1490 peers.append(unit)
1491 return peers
1492
1493
1494+def peer_ips(peer_relation='cluster', addr_key='private-address'):
1495+ '''Return a dict of peers and their private-address'''
1496+ peers = {}
1497+ for r_id in relation_ids(peer_relation):
1498+ for unit in relation_list(r_id):
1499+ peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
1500+ return peers
1501+
1502+
1503 def oldest_peer(peers):
1504+ """Determines who the oldest peer is by comparing unit numbers."""
1505 local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
1506 for peer in peers:
1507 remote_unit_no = int(peer.split('/')[1])
1508@@ -72,16 +199,9 @@
1509
1510
1511 def eligible_leader(resource):
1512- if is_clustered():
1513- if not is_leader(resource):
1514- log('Deferring action to CRM leader.', level=INFO)
1515- return False
1516- else:
1517- peers = peer_units()
1518- if peers and not oldest_peer(peers):
1519- log('Deferring action to oldest service unit.', level=INFO)
1520- return False
1521- return True
1522+ log("eligible_leader is deprecated. Please consider using "
1523+ "is_elected_leader instead.", level=WARNING)
1524+ return is_elected_leader(resource)
1525
1526
1527 def https():
1528@@ -91,16 +211,16 @@
1529 .
1530 returns: boolean
1531 '''
1532- if config_get('use-https') == "yes":
1533+ use_https = config_get('use-https')
1534+ if use_https and bool_from_string(use_https):
1535 return True
1536 if config_get('ssl_cert') and config_get('ssl_key'):
1537 return True
1538 for r_id in relation_ids('identity-service'):
1539 for unit in relation_list(r_id):
1540+ # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
1541 rel_state = [
1542 relation_get('https_keystone', rid=r_id, unit=unit),
1543- relation_get('ssl_cert', rid=r_id, unit=unit),
1544- relation_get('ssl_key', rid=r_id, unit=unit),
1545 relation_get('ca_cert', rid=r_id, unit=unit),
1546 ]
1547 # NOTE: works around (LP: #1203241)
1548@@ -109,54 +229,66 @@
1549 return False
1550
1551
1552-def determine_api_port(public_port):
1553+def determine_api_port(public_port, singlenode_mode=False):
1554 '''
1555 Determine correct API server listening port based on
1556 existence of HTTPS reverse proxy and/or haproxy.
1557
1558 public_port: int: standard public port for given service
1559
1560+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
1561+
1562 returns: int: the correct listening port for the API service
1563 '''
1564 i = 0
1565- if len(peer_units()) > 0 or is_clustered():
1566+ if singlenode_mode:
1567+ i += 1
1568+ elif len(peer_units()) > 0 or is_clustered():
1569 i += 1
1570 if https():
1571 i += 1
1572 return public_port - (i * 10)
1573
1574
1575-def determine_apache_port(public_port):
1576+def determine_apache_port(public_port, singlenode_mode=False):
1577 '''
1578 Description: Determine correct apache listening port based on public IP +
1579 state of the cluster.
1580
1581 public_port: int: standard public port for given service
1582
1583+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
1584+
1585 returns: int: the correct listening port for the HAProxy service
1586 '''
1587 i = 0
1588- if len(peer_units()) > 0 or is_clustered():
1589+ if singlenode_mode:
1590+ i += 1
1591+ elif len(peer_units()) > 0 or is_clustered():
1592 i += 1
1593 return public_port - (i * 10)
1594
1595
1596-def get_hacluster_config():
1597+def get_hacluster_config(exclude_keys=None):
1598 '''
1599 Obtains all relevant configuration from charm configuration required
1600 for initiating a relation to hacluster:
1601
1602- ha-bindiface, ha-mcastport, vip, vip_iface, vip_cidr
1603+ ha-bindiface, ha-mcastport, vip
1604
1605+ param: exclude_keys: list of setting key(s) to be excluded.
1606 returns: dict: A dict containing settings keyed by setting name.
1607 raises: HAIncompleteConfig if settings are missing.
1608 '''
1609- settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'vip_iface', 'vip_cidr']
1610+ settings = ['ha-bindiface', 'ha-mcastport', 'vip']
1611 conf = {}
1612 for setting in settings:
1613+ if exclude_keys and setting in exclude_keys:
1614+ continue
1615+
1616 conf[setting] = config_get(setting)
1617 missing = []
1618- [missing.append(s) for s, v in conf.iteritems() if v is None]
1619+ [missing.append(s) for s, v in six.iteritems(conf) if v is None]
1620 if missing:
1621 log('Insufficient config data to configure hacluster.', level=ERROR)
1622 raise HAIncompleteConfig
1623@@ -170,6 +302,7 @@
1624
1625 :configs : OSTemplateRenderer: A config tempating object to inspect for
1626 a complete https context.
1627+
1628 :vip_setting: str: Setting in charm config that specifies
1629 VIP address.
1630 '''
1631
1632=== modified file 'hooks/charmhelpers/contrib/network/__init__.py'
1633--- hooks/charmhelpers/contrib/network/__init__.py 2014-06-05 10:59:23 +0000
1634+++ hooks/charmhelpers/contrib/network/__init__.py 2015-11-12 11:46:11 +0000
1635@@ -0,0 +1,15 @@
1636+# Copyright 2014-2015 Canonical Limited.
1637+#
1638+# This file is part of charm-helpers.
1639+#
1640+# charm-helpers is free software: you can redistribute it and/or modify
1641+# it under the terms of the GNU Lesser General Public License version 3 as
1642+# published by the Free Software Foundation.
1643+#
1644+# charm-helpers is distributed in the hope that it will be useful,
1645+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1646+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1647+# GNU Lesser General Public License for more details.
1648+#
1649+# You should have received a copy of the GNU Lesser General Public License
1650+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1651
1652=== added file 'hooks/charmhelpers/contrib/network/ip.py'
1653--- hooks/charmhelpers/contrib/network/ip.py 1970-01-01 00:00:00 +0000
1654+++ hooks/charmhelpers/contrib/network/ip.py 2015-11-12 11:46:11 +0000
1655@@ -0,0 +1,456 @@
1656+# Copyright 2014-2015 Canonical Limited.
1657+#
1658+# This file is part of charm-helpers.
1659+#
1660+# charm-helpers is free software: you can redistribute it and/or modify
1661+# it under the terms of the GNU Lesser General Public License version 3 as
1662+# published by the Free Software Foundation.
1663+#
1664+# charm-helpers is distributed in the hope that it will be useful,
1665+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1666+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1667+# GNU Lesser General Public License for more details.
1668+#
1669+# You should have received a copy of the GNU Lesser General Public License
1670+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1671+
1672+import glob
1673+import re
1674+import subprocess
1675+import six
1676+import socket
1677+
1678+from functools import partial
1679+
1680+from charmhelpers.core.hookenv import unit_get
1681+from charmhelpers.fetch import apt_install, apt_update
1682+from charmhelpers.core.hookenv import (
1683+ log,
1684+ WARNING,
1685+)
1686+
1687+try:
1688+ import netifaces
1689+except ImportError:
1690+ apt_update(fatal=True)
1691+ apt_install('python-netifaces', fatal=True)
1692+ import netifaces
1693+
1694+try:
1695+ import netaddr
1696+except ImportError:
1697+ apt_update(fatal=True)
1698+ apt_install('python-netaddr', fatal=True)
1699+ import netaddr
1700+
1701+
1702+def _validate_cidr(network):
1703+ try:
1704+ netaddr.IPNetwork(network)
1705+ except (netaddr.core.AddrFormatError, ValueError):
1706+ raise ValueError("Network (%s) is not in CIDR presentation format" %
1707+ network)
1708+
1709+
1710+def no_ip_found_error_out(network):
1711+ errmsg = ("No IP address found in network: %s" % network)
1712+ raise ValueError(errmsg)
1713+
1714+
1715+def get_address_in_network(network, fallback=None, fatal=False):
1716+ """Get an IPv4 or IPv6 address within the network from the host.
1717+
1718+ :param network (str): CIDR presentation format. For example,
1719+ '192.168.1.0/24'.
1720+ :param fallback (str): If no address is found, return fallback.
1721+ :param fatal (boolean): If no address is found, fallback is not
1722+ set and fatal is True then exit(1).
1723+ """
1724+ if network is None:
1725+ if fallback is not None:
1726+ return fallback
1727+
1728+ if fatal:
1729+ no_ip_found_error_out(network)
1730+ else:
1731+ return None
1732+
1733+ _validate_cidr(network)
1734+ network = netaddr.IPNetwork(network)
1735+ for iface in netifaces.interfaces():
1736+ addresses = netifaces.ifaddresses(iface)
1737+ if network.version == 4 and netifaces.AF_INET in addresses:
1738+ addr = addresses[netifaces.AF_INET][0]['addr']
1739+ netmask = addresses[netifaces.AF_INET][0]['netmask']
1740+ cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
1741+ if cidr in network:
1742+ return str(cidr.ip)
1743+
1744+ if network.version == 6 and netifaces.AF_INET6 in addresses:
1745+ for addr in addresses[netifaces.AF_INET6]:
1746+ if not addr['addr'].startswith('fe80'):
1747+ cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
1748+ addr['netmask']))
1749+ if cidr in network:
1750+ return str(cidr.ip)
1751+
1752+ if fallback is not None:
1753+ return fallback
1754+
1755+ if fatal:
1756+ no_ip_found_error_out(network)
1757+
1758+ return None
1759+
1760+
1761+def is_ipv6(address):
1762+ """Determine whether provided address is IPv6 or not."""
1763+ try:
1764+ address = netaddr.IPAddress(address)
1765+ except netaddr.AddrFormatError:
1766+ # probably a hostname - so not an address at all!
1767+ return False
1768+
1769+ return address.version == 6
1770+
1771+
1772+def is_address_in_network(network, address):
1773+ """
1774+ Determine whether the provided address is within a network range.
1775+
1776+ :param network (str): CIDR presentation format. For example,
1777+ '192.168.1.0/24'.
1778+ :param address: An individual IPv4 or IPv6 address without a net
1779+ mask or subnet prefix. For example, '192.168.1.1'.
1780+ :returns boolean: Flag indicating whether address is in network.
1781+ """
1782+ try:
1783+ network = netaddr.IPNetwork(network)
1784+ except (netaddr.core.AddrFormatError, ValueError):
1785+ raise ValueError("Network (%s) is not in CIDR presentation format" %
1786+ network)
1787+
1788+ try:
1789+ address = netaddr.IPAddress(address)
1790+ except (netaddr.core.AddrFormatError, ValueError):
1791+ raise ValueError("Address (%s) is not in correct presentation format" %
1792+ address)
1793+
1794+ if address in network:
1795+ return True
1796+ else:
1797+ return False
1798+
1799+
1800+def _get_for_address(address, key):
1801+ """Retrieve an attribute of or the physical interface that
1802+ the IP address provided could be bound to.
1803+
1804+ :param address (str): An individual IPv4 or IPv6 address without a net
1805+ mask or subnet prefix. For example, '192.168.1.1'.
1806+ :param key: 'iface' for the physical interface name or an attribute
1807+ of the configured interface, for example 'netmask'.
1808+ :returns str: Requested attribute or None if address is not bindable.
1809+ """
1810+ address = netaddr.IPAddress(address)
1811+ for iface in netifaces.interfaces():
1812+ addresses = netifaces.ifaddresses(iface)
1813+ if address.version == 4 and netifaces.AF_INET in addresses:
1814+ addr = addresses[netifaces.AF_INET][0]['addr']
1815+ netmask = addresses[netifaces.AF_INET][0]['netmask']
1816+ network = netaddr.IPNetwork("%s/%s" % (addr, netmask))
1817+ cidr = network.cidr
1818+ if address in cidr:
1819+ if key == 'iface':
1820+ return iface
1821+ else:
1822+ return addresses[netifaces.AF_INET][0][key]
1823+
1824+ if address.version == 6 and netifaces.AF_INET6 in addresses:
1825+ for addr in addresses[netifaces.AF_INET6]:
1826+ if not addr['addr'].startswith('fe80'):
1827+ network = netaddr.IPNetwork("%s/%s" % (addr['addr'],
1828+ addr['netmask']))
1829+ cidr = network.cidr
1830+ if address in cidr:
1831+ if key == 'iface':
1832+ return iface
1833+ elif key == 'netmask' and cidr:
1834+ return str(cidr).split('/')[1]
1835+ else:
1836+ return addr[key]
1837+
1838+ return None
1839+
1840+
1841+get_iface_for_address = partial(_get_for_address, key='iface')
1842+
1843+
1844+get_netmask_for_address = partial(_get_for_address, key='netmask')
1845+
1846+
1847+def format_ipv6_addr(address):
1848+ """If address is IPv6, wrap it in '[]' otherwise return None.
1849+
1850+ This is required by most configuration files when specifying IPv6
1851+ addresses.
1852+ """
1853+ if is_ipv6(address):
1854+ return "[%s]" % address
1855+
1856+ return None
1857+
1858+
1859+def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
1860+ fatal=True, exc_list=None):
1861+ """Return the assigned IP address for a given interface, if any."""
1862+ # Extract nic if passed /dev/ethX
1863+ if '/' in iface:
1864+ iface = iface.split('/')[-1]
1865+
1866+ if not exc_list:
1867+ exc_list = []
1868+
1869+ try:
1870+ inet_num = getattr(netifaces, inet_type)
1871+ except AttributeError:
1872+ raise Exception("Unknown inet type '%s'" % str(inet_type))
1873+
1874+ interfaces = netifaces.interfaces()
1875+ if inc_aliases:
1876+ ifaces = []
1877+ for _iface in interfaces:
1878+ if iface == _iface or _iface.split(':')[0] == iface:
1879+ ifaces.append(_iface)
1880+
1881+ if fatal and not ifaces:
1882+ raise Exception("Invalid interface '%s'" % iface)
1883+
1884+ ifaces.sort()
1885+ else:
1886+ if iface not in interfaces:
1887+ if fatal:
1888+ raise Exception("Interface '%s' not found " % (iface))
1889+ else:
1890+ return []
1891+
1892+ else:
1893+ ifaces = [iface]
1894+
1895+ addresses = []
1896+ for netiface in ifaces:
1897+ net_info = netifaces.ifaddresses(netiface)
1898+ if inet_num in net_info:
1899+ for entry in net_info[inet_num]:
1900+ if 'addr' in entry and entry['addr'] not in exc_list:
1901+ addresses.append(entry['addr'])
1902+
1903+ if fatal and not addresses:
1904+ raise Exception("Interface '%s' doesn't have any %s addresses." %
1905+ (iface, inet_type))
1906+
1907+ return sorted(addresses)
1908+
1909+
1910+get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
1911+
1912+
1913+def get_iface_from_addr(addr):
1914+ """Work out on which interface the provided address is configured."""
1915+ for iface in netifaces.interfaces():
1916+ addresses = netifaces.ifaddresses(iface)
1917+ for inet_type in addresses:
1918+ for _addr in addresses[inet_type]:
1919+ _addr = _addr['addr']
1920+ # link local
1921+ ll_key = re.compile("(.+)%.*")
1922+ raw = re.match(ll_key, _addr)
1923+ if raw:
1924+ _addr = raw.group(1)
1925+
1926+ if _addr == addr:
1927+ log("Address '%s' is configured on iface '%s'" %
1928+ (addr, iface))
1929+ return iface
1930+
1931+ msg = "Unable to infer net iface on which '%s' is configured" % (addr)
1932+ raise Exception(msg)
1933+
1934+
1935+def sniff_iface(f):
1936+ """Ensure decorated function is called with a value for iface.
1937+
1938+ If no iface provided, inject net iface inferred from unit private address.
1939+ """
1940+ def iface_sniffer(*args, **kwargs):
1941+ if not kwargs.get('iface', None):
1942+ kwargs['iface'] = get_iface_from_addr(unit_get('private-address'))
1943+
1944+ return f(*args, **kwargs)
1945+
1946+ return iface_sniffer
1947+
1948+
1949+@sniff_iface
1950+def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None,
1951+ dynamic_only=True):
1952+ """Get assigned IPv6 address for a given interface.
1953+
1954+ Returns list of addresses found. If no address found, returns empty list.
1955+
1956+ If iface is None, we infer the current primary interface by doing a reverse
1957+ lookup on the unit private-address.
1958+
1959+ We currently only support scope global IPv6 addresses i.e. non-temporary
1960+ addresses. If no global IPv6 address is found, return the first one found
1961+ in the ipv6 address list.
1962+ """
1963+ addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
1964+ inc_aliases=inc_aliases, fatal=fatal,
1965+ exc_list=exc_list)
1966+
1967+ if addresses:
1968+ global_addrs = []
1969+ for addr in addresses:
1970+ key_scope_link_local = re.compile("^fe80::..(.+)%(.+)")
1971+ m = re.match(key_scope_link_local, addr)
1972+ if m:
1973+ eui_64_mac = m.group(1)
1974+ iface = m.group(2)
1975+ else:
1976+ global_addrs.append(addr)
1977+
1978+ if global_addrs:
1979+ # Make sure any found global addresses are not temporary
1980+ cmd = ['ip', 'addr', 'show', iface]
1981+ out = subprocess.check_output(cmd).decode('UTF-8')
1982+ if dynamic_only:
1983+ key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
1984+ else:
1985+ key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
1986+
1987+ addrs = []
1988+ for line in out.split('\n'):
1989+ line = line.strip()
1990+ m = re.match(key, line)
1991+ if m and 'temporary' not in line:
1992+ # Return the first valid address we find
1993+ for addr in global_addrs:
1994+ if m.group(1) == addr:
1995+ if not dynamic_only or \
1996+ m.group(1).endswith(eui_64_mac):
1997+ addrs.append(addr)
1998+
1999+ if addrs:
2000+ return addrs
2001+
2002+ if fatal:
2003+ raise Exception("Interface '%s' does not have a scope global "
2004+ "non-temporary ipv6 address." % iface)
2005+
2006+ return []
2007+
2008+
2009+def get_bridges(vnic_dir='/sys/devices/virtual/net'):
2010+ """Return a list of bridges on the system."""
2011+ b_regex = "%s/*/bridge" % vnic_dir
2012+ return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_regex)]
2013+
2014+
2015+def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
2016+ """Return a list of nics comprising a given bridge on the system."""
2017+ brif_regex = "%s/%s/brif/*" % (vnic_dir, bridge)
2018+ return [x.split('/')[-1] for x in glob.glob(brif_regex)]
2019+
2020+
2021+def is_bridge_member(nic):
2022+ """Check if a given nic is a member of a bridge."""
2023+ for bridge in get_bridges():
2024+ if nic in get_bridge_nics(bridge):
2025+ return True
2026+
2027+ return False
2028+
2029+
2030+def is_ip(address):
2031+ """
2032+ Returns True if address is a valid IP address.
2033+ """
2034+ try:
2035+ # Test to see if already an IPv4 address
2036+ socket.inet_aton(address)
2037+ return True
2038+ except socket.error:
2039+ return False
2040+
2041+
2042+def ns_query(address):
2043+ try:
2044+ import dns.resolver
2045+ except ImportError:
2046+ apt_install('python-dnspython')
2047+ import dns.resolver
2048+
2049+ if isinstance(address, dns.name.Name):
2050+ rtype = 'PTR'
2051+ elif isinstance(address, six.string_types):
2052+ rtype = 'A'
2053+ else:
2054+ return None
2055+
2056+ answers = dns.resolver.query(address, rtype)
2057+ if answers:
2058+ return str(answers[0])
2059+ return None
2060+
2061+
2062+def get_host_ip(hostname, fallback=None):
2063+ """
2064+ Resolves the IP for a given hostname, or returns
2065+ the input if it is already an IP.
2066+ """
2067+ if is_ip(hostname):
2068+ return hostname
2069+
2070+ ip_addr = ns_query(hostname)
2071+ if not ip_addr:
2072+ try:
2073+ ip_addr = socket.gethostbyname(hostname)
2074+ except:
2075+ log("Failed to resolve hostname '%s'" % (hostname),
2076+ level=WARNING)
2077+ return fallback
2078+ return ip_addr
2079+
2080+
2081+def get_hostname(address, fqdn=True):
2082+ """
2083+ Resolves hostname for given IP, or returns the input
2084+ if it is already a hostname.
2085+ """
2086+ if is_ip(address):
2087+ try:
2088+ import dns.reversename
2089+ except ImportError:
2090+ apt_install("python-dnspython")
2091+ import dns.reversename
2092+
2093+ rev = dns.reversename.from_address(address)
2094+ result = ns_query(rev)
2095+
2096+ if not result:
2097+ try:
2098+ result = socket.gethostbyaddr(address)[0]
2099+ except:
2100+ return None
2101+ else:
2102+ result = address
2103+
2104+ if fqdn:
2105+ # strip trailing .
2106+ if result.endswith('.'):
2107+ return result[:-1]
2108+ else:
2109+ return result
2110+ else:
2111+ return result.split('.')[0]
2112
2113=== modified file 'hooks/charmhelpers/contrib/network/ovs/__init__.py'
2114--- hooks/charmhelpers/contrib/network/ovs/__init__.py 2014-06-05 10:59:23 +0000
2115+++ hooks/charmhelpers/contrib/network/ovs/__init__.py 2015-11-12 11:46:11 +0000
2116@@ -1,3 +1,19 @@
2117+# Copyright 2014-2015 Canonical Limited.
2118+#
2119+# This file is part of charm-helpers.
2120+#
2121+# charm-helpers is free software: you can redistribute it and/or modify
2122+# it under the terms of the GNU Lesser General Public License version 3 as
2123+# published by the Free Software Foundation.
2124+#
2125+# charm-helpers is distributed in the hope that it will be useful,
2126+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2127+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2128+# GNU Lesser General Public License for more details.
2129+#
2130+# You should have received a copy of the GNU Lesser General Public License
2131+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2132+
2133 ''' Helpers for interacting with OpenvSwitch '''
2134 import subprocess
2135 import os
2136@@ -21,12 +37,16 @@
2137 subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-br", name])
2138
2139
2140-def add_bridge_port(name, port):
2141+def add_bridge_port(name, port, promisc=False):
2142 ''' Add a port to the named openvswitch bridge '''
2143 log('Adding port {} to bridge {}'.format(port, name))
2144 subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-port",
2145 name, port])
2146 subprocess.check_call(["ip", "link", "set", port, "up"])
2147+ if promisc:
2148+ subprocess.check_call(["ip", "link", "set", port, "promisc", "on"])
2149+ else:
2150+ subprocess.check_call(["ip", "link", "set", port, "promisc", "off"])
2151
2152
2153 def del_bridge_port(name, port):
2154@@ -35,6 +55,7 @@
2155 subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-port",
2156 name, port])
2157 subprocess.check_call(["ip", "link", "set", port, "down"])
2158+ subprocess.check_call(["ip", "link", "set", port, "promisc", "off"])
2159
2160
2161 def set_manager(manager):
2162
2163=== modified file 'hooks/charmhelpers/contrib/openstack/__init__.py'
2164--- hooks/charmhelpers/contrib/openstack/__init__.py 2014-06-05 10:59:23 +0000
2165+++ hooks/charmhelpers/contrib/openstack/__init__.py 2015-11-12 11:46:11 +0000
2166@@ -0,0 +1,15 @@
2167+# Copyright 2014-2015 Canonical Limited.
2168+#
2169+# This file is part of charm-helpers.
2170+#
2171+# charm-helpers is free software: you can redistribute it and/or modify
2172+# it under the terms of the GNU Lesser General Public License version 3 as
2173+# published by the Free Software Foundation.
2174+#
2175+# charm-helpers is distributed in the hope that it will be useful,
2176+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2177+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2178+# GNU Lesser General Public License for more details.
2179+#
2180+# You should have received a copy of the GNU Lesser General Public License
2181+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2182
2183=== modified file 'hooks/charmhelpers/contrib/openstack/alternatives.py'
2184--- hooks/charmhelpers/contrib/openstack/alternatives.py 2014-06-05 10:59:23 +0000
2185+++ hooks/charmhelpers/contrib/openstack/alternatives.py 2015-11-12 11:46:11 +0000
2186@@ -1,3 +1,19 @@
2187+# Copyright 2014-2015 Canonical Limited.
2188+#
2189+# This file is part of charm-helpers.
2190+#
2191+# charm-helpers is free software: you can redistribute it and/or modify
2192+# it under the terms of the GNU Lesser General Public License version 3 as
2193+# published by the Free Software Foundation.
2194+#
2195+# charm-helpers is distributed in the hope that it will be useful,
2196+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2197+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2198+# GNU Lesser General Public License for more details.
2199+#
2200+# You should have received a copy of the GNU Lesser General Public License
2201+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2202+
2203 ''' Helper for managing alternatives for file conflict resolution '''
2204
2205 import subprocess
2206
2207=== added directory 'hooks/charmhelpers/contrib/openstack/amulet'
2208=== added file 'hooks/charmhelpers/contrib/openstack/amulet/__init__.py'
2209--- hooks/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
2210+++ hooks/charmhelpers/contrib/openstack/amulet/__init__.py 2015-11-12 11:46:11 +0000
2211@@ -0,0 +1,15 @@
2212+# Copyright 2014-2015 Canonical Limited.
2213+#
2214+# This file is part of charm-helpers.
2215+#
2216+# charm-helpers is free software: you can redistribute it and/or modify
2217+# it under the terms of the GNU Lesser General Public License version 3 as
2218+# published by the Free Software Foundation.
2219+#
2220+# charm-helpers is distributed in the hope that it will be useful,
2221+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2222+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2223+# GNU Lesser General Public License for more details.
2224+#
2225+# You should have received a copy of the GNU Lesser General Public License
2226+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2227
2228=== added file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
2229--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
2230+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-11-12 11:46:11 +0000
2231@@ -0,0 +1,197 @@
2232+# Copyright 2014-2015 Canonical Limited.
2233+#
2234+# This file is part of charm-helpers.
2235+#
2236+# charm-helpers is free software: you can redistribute it and/or modify
2237+# it under the terms of the GNU Lesser General Public License version 3 as
2238+# published by the Free Software Foundation.
2239+#
2240+# charm-helpers is distributed in the hope that it will be useful,
2241+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2242+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2243+# GNU Lesser General Public License for more details.
2244+#
2245+# You should have received a copy of the GNU Lesser General Public License
2246+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2247+
2248+import six
2249+from collections import OrderedDict
2250+from charmhelpers.contrib.amulet.deployment import (
2251+ AmuletDeployment
2252+)
2253+
2254+
2255+class OpenStackAmuletDeployment(AmuletDeployment):
2256+ """OpenStack amulet deployment.
2257+
2258+ This class inherits from AmuletDeployment and has additional support
2259+ that is specifically for use by OpenStack charms.
2260+ """
2261+
2262+ def __init__(self, series=None, openstack=None, source=None, stable=True):
2263+ """Initialize the deployment environment."""
2264+ super(OpenStackAmuletDeployment, self).__init__(series)
2265+ self.openstack = openstack
2266+ self.source = source
2267+ self.stable = stable
2268+ # Note(coreycb): this needs to be changed when new next branches come
2269+ # out.
2270+ self.current_next = "trusty"
2271+
2272+ def _determine_branch_locations(self, other_services):
2273+ """Determine the branch locations for the other services.
2274+
2275+ Determine if the local branch being tested is derived from its
2276+ stable or next (dev) branch, and based on this, use the corresonding
2277+ stable or next branches for the other_services."""
2278+
2279+ # Charms outside the lp:~openstack-charmers namespace
2280+ base_charms = ['mysql', 'mongodb', 'nrpe']
2281+
2282+ # Force these charms to current series even when using an older series.
2283+ # ie. Use trusty/nrpe even when series is precise, as the P charm
2284+ # does not possess the necessary external master config and hooks.
2285+ force_series_current = ['nrpe']
2286+
2287+ if self.series in ['precise', 'trusty']:
2288+ base_series = self.series
2289+ else:
2290+ base_series = self.current_next
2291+
2292+ for svc in other_services:
2293+ if svc['name'] in force_series_current:
2294+ base_series = self.current_next
2295+ # If a location has been explicitly set, use it
2296+ if svc.get('location'):
2297+ continue
2298+ if self.stable:
2299+ temp = 'lp:charms/{}/{}'
2300+ svc['location'] = temp.format(base_series,
2301+ svc['name'])
2302+ else:
2303+ if svc['name'] in base_charms:
2304+ temp = 'lp:charms/{}/{}'
2305+ svc['location'] = temp.format(base_series,
2306+ svc['name'])
2307+ else:
2308+ temp = 'lp:~openstack-charmers/charms/{}/{}/next'
2309+ svc['location'] = temp.format(self.current_next,
2310+ svc['name'])
2311+
2312+ return other_services
2313+
2314+ def _add_services(self, this_service, other_services):
2315+ """Add services to the deployment and set openstack-origin/source."""
2316+ other_services = self._determine_branch_locations(other_services)
2317+
2318+ super(OpenStackAmuletDeployment, self)._add_services(this_service,
2319+ other_services)
2320+
2321+ services = other_services
2322+ services.append(this_service)
2323+
2324+ # Charms which should use the source config option
2325+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
2326+ 'ceph-osd', 'ceph-radosgw']
2327+
2328+ # Charms which can not use openstack-origin, ie. many subordinates
2329+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
2330+
2331+ if self.openstack:
2332+ for svc in services:
2333+ if svc['name'] not in use_source + no_origin:
2334+ config = {'openstack-origin': self.openstack}
2335+ self.d.configure(svc['name'], config)
2336+
2337+ if self.source:
2338+ for svc in services:
2339+ if svc['name'] in use_source and svc['name'] not in no_origin:
2340+ config = {'source': self.source}
2341+ self.d.configure(svc['name'], config)
2342+
2343+ def _configure_services(self, configs):
2344+ """Configure all of the services."""
2345+ for service, config in six.iteritems(configs):
2346+ self.d.configure(service, config)
2347+
2348+ def _get_openstack_release(self):
2349+ """Get openstack release.
2350+
2351+ Return an integer representing the enum value of the openstack
2352+ release.
2353+ """
2354+ # Must be ordered by OpenStack release (not by Ubuntu release):
2355+ (self.precise_essex, self.precise_folsom, self.precise_grizzly,
2356+ self.precise_havana, self.precise_icehouse,
2357+ self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
2358+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
2359+ self.wily_liberty) = range(12)
2360+
2361+ releases = {
2362+ ('precise', None): self.precise_essex,
2363+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
2364+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
2365+ ('precise', 'cloud:precise-havana'): self.precise_havana,
2366+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
2367+ ('trusty', None): self.trusty_icehouse,
2368+ ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
2369+ ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
2370+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
2371+ ('utopic', None): self.utopic_juno,
2372+ ('vivid', None): self.vivid_kilo,
2373+ ('wily', None): self.wily_liberty}
2374+ return releases[(self.series, self.openstack)]
2375+
2376+ def _get_openstack_release_string(self):
2377+ """Get openstack release string.
2378+
2379+ Return a string representing the openstack release.
2380+ """
2381+ releases = OrderedDict([
2382+ ('precise', 'essex'),
2383+ ('quantal', 'folsom'),
2384+ ('raring', 'grizzly'),
2385+ ('saucy', 'havana'),
2386+ ('trusty', 'icehouse'),
2387+ ('utopic', 'juno'),
2388+ ('vivid', 'kilo'),
2389+ ('wily', 'liberty'),
2390+ ])
2391+ if self.openstack:
2392+ os_origin = self.openstack.split(':')[1]
2393+ return os_origin.split('%s-' % self.series)[1].split('/')[0]
2394+ else:
2395+ return releases[self.series]
2396+
2397+ def get_ceph_expected_pools(self, radosgw=False):
2398+ """Return a list of expected ceph pools in a ceph + cinder + glance
2399+ test scenario, based on OpenStack release and whether ceph radosgw
2400+ is flagged as present or not."""
2401+
2402+ if self._get_openstack_release() >= self.trusty_kilo:
2403+ # Kilo or later
2404+ pools = [
2405+ 'rbd',
2406+ 'cinder',
2407+ 'glance'
2408+ ]
2409+ else:
2410+ # Juno or earlier
2411+ pools = [
2412+ 'data',
2413+ 'metadata',
2414+ 'rbd',
2415+ 'cinder',
2416+ 'glance'
2417+ ]
2418+
2419+ if radosgw:
2420+ pools.extend([
2421+ '.rgw.root',
2422+ '.rgw.control',
2423+ '.rgw',
2424+ '.rgw.gc',
2425+ '.users.uid'
2426+ ])
2427+
2428+ return pools
2429
2430=== added file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
2431--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
2432+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-11-12 11:46:11 +0000
2433@@ -0,0 +1,963 @@
2434+# Copyright 2014-2015 Canonical Limited.
2435+#
2436+# This file is part of charm-helpers.
2437+#
2438+# charm-helpers is free software: you can redistribute it and/or modify
2439+# it under the terms of the GNU Lesser General Public License version 3 as
2440+# published by the Free Software Foundation.
2441+#
2442+# charm-helpers is distributed in the hope that it will be useful,
2443+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2444+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2445+# GNU Lesser General Public License for more details.
2446+#
2447+# You should have received a copy of the GNU Lesser General Public License
2448+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2449+
2450+import amulet
2451+import json
2452+import logging
2453+import os
2454+import six
2455+import time
2456+import urllib
2457+
2458+import cinderclient.v1.client as cinder_client
2459+import glanceclient.v1.client as glance_client
2460+import heatclient.v1.client as heat_client
2461+import keystoneclient.v2_0 as keystone_client
2462+import novaclient.v1_1.client as nova_client
2463+import pika
2464+import swiftclient
2465+
2466+from charmhelpers.contrib.amulet.utils import (
2467+ AmuletUtils
2468+)
2469+
2470+DEBUG = logging.DEBUG
2471+ERROR = logging.ERROR
2472+
2473+
2474+class OpenStackAmuletUtils(AmuletUtils):
2475+ """OpenStack amulet utilities.
2476+
2477+ This class inherits from AmuletUtils and has additional support
2478+ that is specifically for use by OpenStack charm tests.
2479+ """
2480+
2481+ def __init__(self, log_level=ERROR):
2482+ """Initialize the deployment environment."""
2483+ super(OpenStackAmuletUtils, self).__init__(log_level)
2484+
2485+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
2486+ public_port, expected):
2487+ """Validate endpoint data.
2488+
2489+ Validate actual endpoint data vs expected endpoint data. The ports
2490+ are used to find the matching endpoint.
2491+ """
2492+ self.log.debug('Validating endpoint data...')
2493+ self.log.debug('actual: {}'.format(repr(endpoints)))
2494+ found = False
2495+ for ep in endpoints:
2496+ self.log.debug('endpoint: {}'.format(repr(ep)))
2497+ if (admin_port in ep.adminurl and
2498+ internal_port in ep.internalurl and
2499+ public_port in ep.publicurl):
2500+ found = True
2501+ actual = {'id': ep.id,
2502+ 'region': ep.region,
2503+ 'adminurl': ep.adminurl,
2504+ 'internalurl': ep.internalurl,
2505+ 'publicurl': ep.publicurl,
2506+ 'service_id': ep.service_id}
2507+ ret = self._validate_dict_data(expected, actual)
2508+ if ret:
2509+ return 'unexpected endpoint data - {}'.format(ret)
2510+
2511+ if not found:
2512+ return 'endpoint not found'
2513+
2514+ def validate_svc_catalog_endpoint_data(self, expected, actual):
2515+ """Validate service catalog endpoint data.
2516+
2517+ Validate a list of actual service catalog endpoints vs a list of
2518+ expected service catalog endpoints.
2519+ """
2520+ self.log.debug('Validating service catalog endpoint data...')
2521+ self.log.debug('actual: {}'.format(repr(actual)))
2522+ for k, v in six.iteritems(expected):
2523+ if k in actual:
2524+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
2525+ if ret:
2526+ return self.endpoint_error(k, ret)
2527+ else:
2528+ return "endpoint {} does not exist".format(k)
2529+ return ret
2530+
2531+ def validate_tenant_data(self, expected, actual):
2532+ """Validate tenant data.
2533+
2534+ Validate a list of actual tenant data vs list of expected tenant
2535+ data.
2536+ """
2537+ self.log.debug('Validating tenant data...')
2538+ self.log.debug('actual: {}'.format(repr(actual)))
2539+ for e in expected:
2540+ found = False
2541+ for act in actual:
2542+ a = {'enabled': act.enabled, 'description': act.description,
2543+ 'name': act.name, 'id': act.id}
2544+ if e['name'] == a['name']:
2545+ found = True
2546+ ret = self._validate_dict_data(e, a)
2547+ if ret:
2548+ return "unexpected tenant data - {}".format(ret)
2549+ if not found:
2550+ return "tenant {} does not exist".format(e['name'])
2551+ return ret
2552+
2553+ def validate_role_data(self, expected, actual):
2554+ """Validate role data.
2555+
2556+ Validate a list of actual role data vs a list of expected role
2557+ data.
2558+ """
2559+ self.log.debug('Validating role data...')
2560+ self.log.debug('actual: {}'.format(repr(actual)))
2561+ for e in expected:
2562+ found = False
2563+ for act in actual:
2564+ a = {'name': act.name, 'id': act.id}
2565+ if e['name'] == a['name']:
2566+ found = True
2567+ ret = self._validate_dict_data(e, a)
2568+ if ret:
2569+ return "unexpected role data - {}".format(ret)
2570+ if not found:
2571+ return "role {} does not exist".format(e['name'])
2572+ return ret
2573+
2574+ def validate_user_data(self, expected, actual):
2575+ """Validate user data.
2576+
2577+ Validate a list of actual user data vs a list of expected user
2578+ data.
2579+ """
2580+ self.log.debug('Validating user data...')
2581+ self.log.debug('actual: {}'.format(repr(actual)))
2582+ for e in expected:
2583+ found = False
2584+ for act in actual:
2585+ a = {'enabled': act.enabled, 'name': act.name,
2586+ 'email': act.email, 'tenantId': act.tenantId,
2587+ 'id': act.id}
2588+ if e['name'] == a['name']:
2589+ found = True
2590+ ret = self._validate_dict_data(e, a)
2591+ if ret:
2592+ return "unexpected user data - {}".format(ret)
2593+ if not found:
2594+ return "user {} does not exist".format(e['name'])
2595+ return ret
2596+
2597+ def validate_flavor_data(self, expected, actual):
2598+ """Validate flavor data.
2599+
2600+ Validate a list of actual flavors vs a list of expected flavors.
2601+ """
2602+ self.log.debug('Validating flavor data...')
2603+ self.log.debug('actual: {}'.format(repr(actual)))
2604+ act = [a.name for a in actual]
2605+ return self._validate_list_data(expected, act)
2606+
2607+ def tenant_exists(self, keystone, tenant):
2608+ """Return True if tenant exists."""
2609+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
2610+ return tenant in [t.name for t in keystone.tenants.list()]
2611+
2612+ def authenticate_cinder_admin(self, keystone_sentry, username,
2613+ password, tenant):
2614+ """Authenticates admin user with cinder."""
2615+ # NOTE(beisner): cinder python client doesn't accept tokens.
2616+ service_ip = \
2617+ keystone_sentry.relation('shared-db',
2618+ 'mysql:shared-db')['private-address']
2619+ ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
2620+ return cinder_client.Client(username, password, tenant, ept)
2621+
2622+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
2623+ tenant):
2624+ """Authenticates admin user with the keystone admin endpoint."""
2625+ self.log.debug('Authenticating keystone admin...')
2626+ unit = keystone_sentry
2627+ service_ip = unit.relation('shared-db',
2628+ 'mysql:shared-db')['private-address']
2629+ ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
2630+ return keystone_client.Client(username=user, password=password,
2631+ tenant_name=tenant, auth_url=ep)
2632+
2633+ def authenticate_keystone_user(self, keystone, user, password, tenant):
2634+ """Authenticates a regular user with the keystone public endpoint."""
2635+ self.log.debug('Authenticating keystone user ({})...'.format(user))
2636+ ep = keystone.service_catalog.url_for(service_type='identity',
2637+ endpoint_type='publicURL')
2638+ return keystone_client.Client(username=user, password=password,
2639+ tenant_name=tenant, auth_url=ep)
2640+
2641+ def authenticate_glance_admin(self, keystone):
2642+ """Authenticates admin user with glance."""
2643+ self.log.debug('Authenticating glance admin...')
2644+ ep = keystone.service_catalog.url_for(service_type='image',
2645+ endpoint_type='adminURL')
2646+ return glance_client.Client(ep, token=keystone.auth_token)
2647+
2648+ def authenticate_heat_admin(self, keystone):
2649+ """Authenticates the admin user with heat."""
2650+ self.log.debug('Authenticating heat admin...')
2651+ ep = keystone.service_catalog.url_for(service_type='orchestration',
2652+ endpoint_type='publicURL')
2653+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
2654+
2655+ def authenticate_nova_user(self, keystone, user, password, tenant):
2656+ """Authenticates a regular user with nova-api."""
2657+ self.log.debug('Authenticating nova user ({})...'.format(user))
2658+ ep = keystone.service_catalog.url_for(service_type='identity',
2659+ endpoint_type='publicURL')
2660+ return nova_client.Client(username=user, api_key=password,
2661+ project_id=tenant, auth_url=ep)
2662+
2663+ def authenticate_swift_user(self, keystone, user, password, tenant):
2664+ """Authenticates a regular user with swift api."""
2665+ self.log.debug('Authenticating swift user ({})...'.format(user))
2666+ ep = keystone.service_catalog.url_for(service_type='identity',
2667+ endpoint_type='publicURL')
2668+ return swiftclient.Connection(authurl=ep,
2669+ user=user,
2670+ key=password,
2671+ tenant_name=tenant,
2672+ auth_version='2.0')
2673+
2674+ def create_cirros_image(self, glance, image_name):
2675+ """Download the latest cirros image and upload it to glance,
2676+ validate and return a resource pointer.
2677+
2678+ :param glance: pointer to authenticated glance connection
2679+ :param image_name: display name for new image
2680+ :returns: glance image pointer
2681+ """
2682+ self.log.debug('Creating glance cirros image '
2683+ '({})...'.format(image_name))
2684+
2685+ # Download cirros image
2686+ http_proxy = os.getenv('AMULET_HTTP_PROXY')
2687+ self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
2688+ if http_proxy:
2689+ proxies = {'http': http_proxy}
2690+ opener = urllib.FancyURLopener(proxies)
2691+ else:
2692+ opener = urllib.FancyURLopener()
2693+
2694+ f = opener.open('http://download.cirros-cloud.net/version/released')
2695+ version = f.read().strip()
2696+ cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
2697+ local_path = os.path.join('tests', cirros_img)
2698+
2699+ if not os.path.exists(local_path):
2700+ cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
2701+ version, cirros_img)
2702+ opener.retrieve(cirros_url, local_path)
2703+ f.close()
2704+
2705+ # Create glance image
2706+ with open(local_path) as f:
2707+ image = glance.images.create(name=image_name, is_public=True,
2708+ disk_format='qcow2',
2709+ container_format='bare', data=f)
2710+
2711+ # Wait for image to reach active status
2712+ img_id = image.id
2713+ ret = self.resource_reaches_status(glance.images, img_id,
2714+ expected_stat='active',
2715+ msg='Image status wait')
2716+ if not ret:
2717+ msg = 'Glance image failed to reach expected state.'
2718+ amulet.raise_status(amulet.FAIL, msg=msg)
2719+
2720+ # Re-validate new image
2721+ self.log.debug('Validating image attributes...')
2722+ val_img_name = glance.images.get(img_id).name
2723+ val_img_stat = glance.images.get(img_id).status
2724+ val_img_pub = glance.images.get(img_id).is_public
2725+ val_img_cfmt = glance.images.get(img_id).container_format
2726+ val_img_dfmt = glance.images.get(img_id).disk_format
2727+ msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
2728+ 'container fmt:{} disk fmt:{}'.format(
2729+ val_img_name, val_img_pub, img_id,
2730+ val_img_stat, val_img_cfmt, val_img_dfmt))
2731+
2732+ if val_img_name == image_name and val_img_stat == 'active' \
2733+ and val_img_pub is True and val_img_cfmt == 'bare' \
2734+ and val_img_dfmt == 'qcow2':
2735+ self.log.debug(msg_attr)
2736+ else:
2737+ msg = ('Volume validation failed, {}'.format(msg_attr))
2738+ amulet.raise_status(amulet.FAIL, msg=msg)
2739+
2740+ return image
2741+
2742+ def delete_image(self, glance, image):
2743+ """Delete the specified image."""
2744+
2745+ # /!\ DEPRECATION WARNING
2746+ self.log.warn('/!\\ DEPRECATION WARNING: use '
2747+ 'delete_resource instead of delete_image.')
2748+ self.log.debug('Deleting glance image ({})...'.format(image))
2749+ return self.delete_resource(glance.images, image, msg='glance image')
2750+
2751+ def create_instance(self, nova, image_name, instance_name, flavor):
2752+ """Create the specified instance."""
2753+ self.log.debug('Creating instance '
2754+ '({}|{}|{})'.format(instance_name, image_name, flavor))
2755+ image = nova.images.find(name=image_name)
2756+ flavor = nova.flavors.find(name=flavor)
2757+ instance = nova.servers.create(name=instance_name, image=image,
2758+ flavor=flavor)
2759+
2760+ count = 1
2761+ status = instance.status
2762+ while status != 'ACTIVE' and count < 60:
2763+ time.sleep(3)
2764+ instance = nova.servers.get(instance.id)
2765+ status = instance.status
2766+ self.log.debug('instance status: {}'.format(status))
2767+ count += 1
2768+
2769+ if status != 'ACTIVE':
2770+ self.log.error('instance creation timed out')
2771+ return None
2772+
2773+ return instance
2774+
2775+ def delete_instance(self, nova, instance):
2776+ """Delete the specified instance."""
2777+
2778+ # /!\ DEPRECATION WARNING
2779+ self.log.warn('/!\\ DEPRECATION WARNING: use '
2780+ 'delete_resource instead of delete_instance.')
2781+ self.log.debug('Deleting instance ({})...'.format(instance))
2782+ return self.delete_resource(nova.servers, instance,
2783+ msg='nova instance')
2784+
2785+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
2786+ """Create a new keypair, or return pointer if it already exists."""
2787+ try:
2788+ _keypair = nova.keypairs.get(keypair_name)
2789+ self.log.debug('Keypair ({}) already exists, '
2790+ 'using it.'.format(keypair_name))
2791+ return _keypair
2792+ except:
2793+ self.log.debug('Keypair ({}) does not exist, '
2794+ 'creating it.'.format(keypair_name))
2795+
2796+ _keypair = nova.keypairs.create(name=keypair_name)
2797+ return _keypair
2798+
2799+ def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
2800+ img_id=None, src_vol_id=None, snap_id=None):
2801+ """Create cinder volume, optionally from a glance image, OR
2802+ optionally as a clone of an existing volume, OR optionally
2803+ from a snapshot. Wait for the new volume status to reach
2804+ the expected status, validate and return a resource pointer.
2805+
2806+ :param vol_name: cinder volume display name
2807+ :param vol_size: size in gigabytes
2808+ :param img_id: optional glance image id
2809+ :param src_vol_id: optional source volume id to clone
2810+ :param snap_id: optional snapshot id to use
2811+ :returns: cinder volume pointer
2812+ """
2813+ # Handle parameter input and avoid impossible combinations
2814+ if img_id and not src_vol_id and not snap_id:
2815+ # Create volume from image
2816+ self.log.debug('Creating cinder volume from glance image...')
2817+ bootable = 'true'
2818+ elif src_vol_id and not img_id and not snap_id:
2819+ # Clone an existing volume
2820+ self.log.debug('Cloning cinder volume...')
2821+ bootable = cinder.volumes.get(src_vol_id).bootable
2822+ elif snap_id and not src_vol_id and not img_id:
2823+ # Create volume from snapshot
2824+ self.log.debug('Creating cinder volume from snapshot...')
2825+ snap = cinder.volume_snapshots.find(id=snap_id)
2826+ vol_size = snap.size
2827+ snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
2828+ bootable = cinder.volumes.get(snap_vol_id).bootable
2829+ elif not img_id and not src_vol_id and not snap_id:
2830+ # Create volume
2831+ self.log.debug('Creating cinder volume...')
2832+ bootable = 'false'
2833+ else:
2834+ # Impossible combination of parameters
2835+ msg = ('Invalid method use - name:{} size:{} img_id:{} '
2836+ 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
2837+ img_id, src_vol_id,
2838+ snap_id))
2839+ amulet.raise_status(amulet.FAIL, msg=msg)
2840+
2841+ # Create new volume
2842+ try:
2843+ vol_new = cinder.volumes.create(display_name=vol_name,
2844+ imageRef=img_id,
2845+ size=vol_size,
2846+ source_volid=src_vol_id,
2847+ snapshot_id=snap_id)
2848+ vol_id = vol_new.id
2849+ except Exception as e:
2850+ msg = 'Failed to create volume: {}'.format(e)
2851+ amulet.raise_status(amulet.FAIL, msg=msg)
2852+
2853+ # Wait for volume to reach available status
2854+ ret = self.resource_reaches_status(cinder.volumes, vol_id,
2855+ expected_stat="available",
2856+ msg="Volume status wait")
2857+ if not ret:
2858+ msg = 'Cinder volume failed to reach expected state.'
2859+ amulet.raise_status(amulet.FAIL, msg=msg)
2860+
2861+ # Re-validate new volume
2862+ self.log.debug('Validating volume attributes...')
2863+ val_vol_name = cinder.volumes.get(vol_id).display_name
2864+ val_vol_boot = cinder.volumes.get(vol_id).bootable
2865+ val_vol_stat = cinder.volumes.get(vol_id).status
2866+ val_vol_size = cinder.volumes.get(vol_id).size
2867+ msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
2868+ '{} size:{}'.format(val_vol_name, vol_id,
2869+ val_vol_stat, val_vol_boot,
2870+ val_vol_size))
2871+
2872+ if val_vol_boot == bootable and val_vol_stat == 'available' \
2873+ and val_vol_name == vol_name and val_vol_size == vol_size:
2874+ self.log.debug(msg_attr)
2875+ else:
2876+ msg = ('Volume validation failed, {}'.format(msg_attr))
2877+ amulet.raise_status(amulet.FAIL, msg=msg)
2878+
2879+ return vol_new
2880+
2881+ def delete_resource(self, resource, resource_id,
2882+ msg="resource", max_wait=120):
2883+ """Delete one openstack resource, such as one instance, keypair,
2884+ image, volume, stack, etc., and confirm deletion within max wait time.
2885+
2886+ :param resource: pointer to os resource type, ex:glance_client.images
2887+ :param resource_id: unique name or id for the openstack resource
2888+ :param msg: text to identify purpose in logging
2889+ :param max_wait: maximum wait time in seconds
2890+ :returns: True if successful, otherwise False
2891+ """
2892+ self.log.debug('Deleting OpenStack resource '
2893+ '{} ({})'.format(resource_id, msg))
2894+ num_before = len(list(resource.list()))
2895+ resource.delete(resource_id)
2896+
2897+ tries = 0
2898+ num_after = len(list(resource.list()))
2899+ while num_after != (num_before - 1) and tries < (max_wait / 4):
2900+ self.log.debug('{} delete check: '
2901+ '{} [{}:{}] {}'.format(msg, tries,
2902+ num_before,
2903+ num_after,
2904+ resource_id))
2905+ time.sleep(4)
2906+ num_after = len(list(resource.list()))
2907+ tries += 1
2908+
2909+ self.log.debug('{}: expected, actual count = {}, '
2910+ '{}'.format(msg, num_before - 1, num_after))
2911+
2912+ if num_after == (num_before - 1):
2913+ return True
2914+ else:
2915+ self.log.error('{} delete timed out'.format(msg))
2916+ return False
2917+
2918+ def resource_reaches_status(self, resource, resource_id,
2919+ expected_stat='available',
2920+ msg='resource', max_wait=120):
2921+ """Wait for an openstack resources status to reach an
2922+ expected status within a specified time. Useful to confirm that
2923+ nova instances, cinder vols, snapshots, glance images, heat stacks
2924+ and other resources eventually reach the expected status.
2925+
2926+ :param resource: pointer to os resource type, ex: heat_client.stacks
2927+ :param resource_id: unique id for the openstack resource
2928+ :param expected_stat: status to expect resource to reach
2929+ :param msg: text to identify purpose in logging
2930+ :param max_wait: maximum wait time in seconds
2931+ :returns: True if successful, False if status is not reached
2932+ """
2933+
2934+ tries = 0
2935+ resource_stat = resource.get(resource_id).status
2936+ while resource_stat != expected_stat and tries < (max_wait / 4):
2937+ self.log.debug('{} status check: '
2938+ '{} [{}:{}] {}'.format(msg, tries,
2939+ resource_stat,
2940+ expected_stat,
2941+ resource_id))
2942+ time.sleep(4)
2943+ resource_stat = resource.get(resource_id).status
2944+ tries += 1
2945+
2946+ self.log.debug('{}: expected, actual status = {}, '
2947+ '{}'.format(msg, resource_stat, expected_stat))
2948+
2949+ if resource_stat == expected_stat:
2950+ return True
2951+ else:
2952+ self.log.debug('{} never reached expected status: '
2953+ '{}'.format(resource_id, expected_stat))
2954+ return False
2955+
2956+ def get_ceph_osd_id_cmd(self, index):
2957+ """Produce a shell command that will return a ceph-osd id."""
2958+ return ("`initctl list | grep 'ceph-osd ' | "
2959+ "awk 'NR=={} {{ print $2 }}' | "
2960+ "grep -o '[0-9]*'`".format(index + 1))
2961+
2962+ def get_ceph_pools(self, sentry_unit):
2963+ """Return a dict of ceph pools from a single ceph unit, with
2964+ pool name as keys, pool id as vals."""
2965+ pools = {}
2966+ cmd = 'sudo ceph osd lspools'
2967+ output, code = sentry_unit.run(cmd)
2968+ if code != 0:
2969+ msg = ('{} `{}` returned {} '
2970+ '{}'.format(sentry_unit.info['unit_name'],
2971+ cmd, code, output))
2972+ amulet.raise_status(amulet.FAIL, msg=msg)
2973+
2974+ # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
2975+ for pool in str(output).split(','):
2976+ pool_id_name = pool.split(' ')
2977+ if len(pool_id_name) == 2:
2978+ pool_id = pool_id_name[0]
2979+ pool_name = pool_id_name[1]
2980+ pools[pool_name] = int(pool_id)
2981+
2982+ self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
2983+ pools))
2984+ return pools
2985+
2986+ def get_ceph_df(self, sentry_unit):
2987+ """Return dict of ceph df json output, including ceph pool state.
2988+
2989+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
2990+ :returns: Dict of ceph df output
2991+ """
2992+ cmd = 'sudo ceph df --format=json'
2993+ output, code = sentry_unit.run(cmd)
2994+ if code != 0:
2995+ msg = ('{} `{}` returned {} '
2996+ '{}'.format(sentry_unit.info['unit_name'],
2997+ cmd, code, output))
2998+ amulet.raise_status(amulet.FAIL, msg=msg)
2999+ return json.loads(output)
3000+
3001+ def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
3002+ """Take a sample of attributes of a ceph pool, returning ceph
3003+ pool name, object count and disk space used for the specified
3004+ pool ID number.
3005+
3006+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
3007+ :param pool_id: Ceph pool ID
3008+ :returns: List of pool name, object count, kb disk space used
3009+ """
3010+ df = self.get_ceph_df(sentry_unit)
3011+ pool_name = df['pools'][pool_id]['name']
3012+ obj_count = df['pools'][pool_id]['stats']['objects']
3013+ kb_used = df['pools'][pool_id]['stats']['kb_used']
3014+ self.log.debug('Ceph {} pool (ID {}): {} objects, '
3015+ '{} kb used'.format(pool_name, pool_id,
3016+ obj_count, kb_used))
3017+ return pool_name, obj_count, kb_used
3018+
3019+ def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
3020+ """Validate ceph pool samples taken over time, such as pool
3021+ object counts or pool kb used, before adding, after adding, and
3022+ after deleting items which affect those pool attributes. The
3023+ 2nd element is expected to be greater than the 1st; 3rd is expected
3024+ to be less than the 2nd.
3025+
3026+ :param samples: List containing 3 data samples
3027+ :param sample_type: String for logging and usage context
3028+ :returns: None if successful, Failure message otherwise
3029+ """
3030+ original, created, deleted = range(3)
3031+ if samples[created] <= samples[original] or \
3032+ samples[deleted] >= samples[created]:
3033+ return ('Ceph {} samples ({}) '
3034+ 'unexpected.'.format(sample_type, samples))
3035+ else:
3036+ self.log.debug('Ceph {} samples (OK): '
3037+ '{}'.format(sample_type, samples))
3038+ return None
3039+
3040+# rabbitmq/amqp specific helpers:
3041+ def add_rmq_test_user(self, sentry_units,
3042+ username="testuser1", password="changeme"):
3043+ """Add a test user via the first rmq juju unit, check connection as
3044+ the new user against all sentry units.
3045+
3046+ :param sentry_units: list of sentry unit pointers
3047+ :param username: amqp user name, default to testuser1
3048+ :param password: amqp user password
3049+ :returns: None if successful. Raise on error.
3050+ """
3051+ self.log.debug('Adding rmq user ({})...'.format(username))
3052+
3053+ # Check that user does not already exist
3054+ cmd_user_list = 'rabbitmqctl list_users'
3055+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
3056+ if username in output:
3057+ self.log.warning('User ({}) already exists, returning '
3058+ 'gracefully.'.format(username))
3059+ return
3060+
3061+ perms = '".*" ".*" ".*"'
3062+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
3063+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
3064+
3065+ # Add user via first unit
3066+ for cmd in cmds:
3067+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
3068+
3069+ # Check connection against the other sentry_units
3070+ self.log.debug('Checking user connect against units...')
3071+ for sentry_unit in sentry_units:
3072+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
3073+ username=username,
3074+ password=password)
3075+ connection.close()
3076+
3077+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
3078+ """Delete a rabbitmq user via the first rmq juju unit.
3079+
3080+ :param sentry_units: list of sentry unit pointers
3081+ :param username: amqp user name, default to testuser1
3082+ :param password: amqp user password
3083+ :returns: None if successful or no such user.
3084+ """
3085+ self.log.debug('Deleting rmq user ({})...'.format(username))
3086+
3087+ # Check that the user exists
3088+ cmd_user_list = 'rabbitmqctl list_users'
3089+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
3090+
3091+ if username not in output:
3092+ self.log.warning('User ({}) does not exist, returning '
3093+ 'gracefully.'.format(username))
3094+ return
3095+
3096+ # Delete the user
3097+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
3098+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
3099+
3100+ def get_rmq_cluster_status(self, sentry_unit):
3101+ """Execute rabbitmq cluster status command on a unit and return
3102+ the full output.
3103+
3104+ :param unit: sentry unit
3105+ :returns: String containing console output of cluster status command
3106+ """
3107+ cmd = 'rabbitmqctl cluster_status'
3108+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
3109+ self.log.debug('{} cluster_status:\n{}'.format(
3110+ sentry_unit.info['unit_name'], output))
3111+ return str(output)
3112+
3113+ def get_rmq_cluster_running_nodes(self, sentry_unit):
3114+ """Parse rabbitmqctl cluster_status output string, return list of
3115+ running rabbitmq cluster nodes.
3116+
3117+ :param unit: sentry unit
3118+ :returns: List containing node names of running nodes
3119+ """
3120+ # NOTE(beisner): rabbitmqctl cluster_status output is not
3121+ # json-parsable, do string chop foo, then json.loads that.
3122+ str_stat = self.get_rmq_cluster_status(sentry_unit)
3123+ if 'running_nodes' in str_stat:
3124+ pos_start = str_stat.find("{running_nodes,") + 15
3125+ pos_end = str_stat.find("]},", pos_start) + 1
3126+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
3127+ run_nodes = json.loads(str_run_nodes)
3128+ return run_nodes
3129+ else:
3130+ return []
3131+
3132+ def validate_rmq_cluster_running_nodes(self, sentry_units):
3133+ """Check that all rmq unit hostnames are represented in the
3134+ cluster_status output of all units.
3135+
3136+ :param host_names: dict of juju unit names to host names
3137+ :param units: list of sentry unit pointers (all rmq units)
3138+ :returns: None if successful, otherwise return error message
3139+ """
3140+ host_names = self.get_unit_hostnames(sentry_units)
3141+ errors = []
3142+
3143+ # Query every unit for cluster_status running nodes
3144+ for query_unit in sentry_units:
3145+ query_unit_name = query_unit.info['unit_name']
3146+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
3147+
3148+ # Confirm that every unit is represented in the queried unit's
3149+ # cluster_status running nodes output.
3150+ for validate_unit in sentry_units:
3151+ val_host_name = host_names[validate_unit.info['unit_name']]
3152+ val_node_name = 'rabbit@{}'.format(val_host_name)
3153+
3154+ if val_node_name not in running_nodes:
3155+ errors.append('Cluster member check failed on {}: {} not '
3156+ 'in {}\n'.format(query_unit_name,
3157+ val_node_name,
3158+ running_nodes))
3159+ if errors:
3160+ return ''.join(errors)
3161+
3162+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
3163+ """Check a single juju rmq unit for ssl and port in the config file."""
3164+ host = sentry_unit.info['public-address']
3165+ unit_name = sentry_unit.info['unit_name']
3166+
3167+ conf_file = '/etc/rabbitmq/rabbitmq.config'
3168+ conf_contents = str(self.file_contents_safe(sentry_unit,
3169+ conf_file, max_wait=16))
3170+ # Checks
3171+ conf_ssl = 'ssl' in conf_contents
3172+ conf_port = str(port) in conf_contents
3173+
3174+ # Port explicitly checked in config
3175+ if port and conf_port and conf_ssl:
3176+ self.log.debug('SSL is enabled @{}:{} '
3177+ '({})'.format(host, port, unit_name))
3178+ return True
3179+ elif port and not conf_port and conf_ssl:
3180+ self.log.debug('SSL is enabled @{} but not on port {} '
3181+ '({})'.format(host, port, unit_name))
3182+ return False
3183+ # Port not checked (useful when checking that ssl is disabled)
3184+ elif not port and conf_ssl:
3185+ self.log.debug('SSL is enabled @{}:{} '
3186+ '({})'.format(host, port, unit_name))
3187+ return True
3188+ elif not port and not conf_ssl:
3189+ self.log.debug('SSL not enabled @{}:{} '
3190+ '({})'.format(host, port, unit_name))
3191+ return False
3192+ else:
3193+ msg = ('Unknown condition when checking SSL status @{}:{} '
3194+ '({})'.format(host, port, unit_name))
3195+ amulet.raise_status(amulet.FAIL, msg)
3196+
3197+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
3198+ """Check that ssl is enabled on rmq juju sentry units.
3199+
3200+ :param sentry_units: list of all rmq sentry units
3201+ :param port: optional ssl port override to validate
3202+ :returns: None if successful, otherwise return error message
3203+ """
3204+ for sentry_unit in sentry_units:
3205+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
3206+ return ('Unexpected condition: ssl is disabled on unit '
3207+ '({})'.format(sentry_unit.info['unit_name']))
3208+ return None
3209+
3210+ def validate_rmq_ssl_disabled_units(self, sentry_units):
3211+ """Check that ssl is enabled on listed rmq juju sentry units.
3212+
3213+ :param sentry_units: list of all rmq sentry units
3214+ :returns: True if successful. Raise on error.
3215+ """
3216+ for sentry_unit in sentry_units:
3217+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
3218+ return ('Unexpected condition: ssl is enabled on unit '
3219+ '({})'.format(sentry_unit.info['unit_name']))
3220+ return None
3221+
3222+ def configure_rmq_ssl_on(self, sentry_units, deployment,
3223+ port=None, max_wait=60):
3224+ """Turn ssl charm config option on, with optional non-default
3225+ ssl port specification. Confirm that it is enabled on every
3226+ unit.
3227+
3228+ :param sentry_units: list of sentry units
3229+ :param deployment: amulet deployment object pointer
3230+ :param port: amqp port, use defaults if None
3231+ :param max_wait: maximum time to wait in seconds to confirm
3232+ :returns: None if successful. Raise on error.
3233+ """
3234+ self.log.debug('Setting ssl charm config option: on')
3235+
3236+ # Enable RMQ SSL
3237+ config = {'ssl': 'on'}
3238+ if port:
3239+ config['ssl_port'] = port
3240+
3241+ deployment.configure('rabbitmq-server', config)
3242+
3243+ # Confirm
3244+ tries = 0
3245+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
3246+ while ret and tries < (max_wait / 4):
3247+ time.sleep(4)
3248+ self.log.debug('Attempt {}: {}'.format(tries, ret))
3249+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
3250+ tries += 1
3251+
3252+ if ret:
3253+ amulet.raise_status(amulet.FAIL, ret)
3254+
3255+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
3256+ """Turn ssl charm config option off, confirm that it is disabled
3257+ on every unit.
3258+
3259+ :param sentry_units: list of sentry units
3260+ :param deployment: amulet deployment object pointer
3261+ :param max_wait: maximum time to wait in seconds to confirm
3262+ :returns: None if successful. Raise on error.
3263+ """
3264+ self.log.debug('Setting ssl charm config option: off')
3265+
3266+ # Disable RMQ SSL
3267+ config = {'ssl': 'off'}
3268+ deployment.configure('rabbitmq-server', config)
3269+
3270+ # Confirm
3271+ tries = 0
3272+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
3273+ while ret and tries < (max_wait / 4):
3274+ time.sleep(4)
3275+ self.log.debug('Attempt {}: {}'.format(tries, ret))
3276+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
3277+ tries += 1
3278+
3279+ if ret:
3280+ amulet.raise_status(amulet.FAIL, ret)
3281+
3282+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
3283+ port=None, fatal=True,
3284+ username="testuser1", password="changeme"):
3285+ """Establish and return a pika amqp connection to the rabbitmq service
3286+ running on a rmq juju unit.
3287+
3288+ :param sentry_unit: sentry unit pointer
3289+ :param ssl: boolean, default to False
3290+ :param port: amqp port, use defaults if None
3291+ :param fatal: boolean, default to True (raises on connect error)
3292+ :param username: amqp user name, default to testuser1
3293+ :param password: amqp user password
3294+ :returns: pika amqp connection pointer or None if failed and non-fatal
3295+ """
3296+ host = sentry_unit.info['public-address']
3297+ unit_name = sentry_unit.info['unit_name']
3298+
3299+ # Default port logic if port is not specified
3300+ if ssl and not port:
3301+ port = 5671
3302+ elif not ssl and not port:
3303+ port = 5672
3304+
3305+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
3306+ '{}...'.format(host, port, unit_name, username))
3307+
3308+ try:
3309+ credentials = pika.PlainCredentials(username, password)
3310+ parameters = pika.ConnectionParameters(host=host, port=port,
3311+ credentials=credentials,
3312+ ssl=ssl,
3313+ connection_attempts=3,
3314+ retry_delay=5,
3315+ socket_timeout=1)
3316+ connection = pika.BlockingConnection(parameters)
3317+ assert connection.server_properties['product'] == 'RabbitMQ'
3318+ self.log.debug('Connect OK')
3319+ return connection
3320+ except Exception as e:
3321+ msg = ('amqp connection failed to {}:{} as '
3322+ '{} ({})'.format(host, port, username, str(e)))
3323+ if fatal:
3324+ amulet.raise_status(amulet.FAIL, msg)
3325+ else:
3326+ self.log.warn(msg)
3327+ return None
3328+
3329+ def publish_amqp_message_by_unit(self, sentry_unit, message,
3330+ queue="test", ssl=False,
3331+ username="testuser1",
3332+ password="changeme",
3333+ port=None):
3334+ """Publish an amqp message to a rmq juju unit.
3335+
3336+ :param sentry_unit: sentry unit pointer
3337+ :param message: amqp message string
3338+ :param queue: message queue, default to test
3339+ :param username: amqp user name, default to testuser1
3340+ :param password: amqp user password
3341+ :param ssl: boolean, default to False
3342+ :param port: amqp port, use defaults if None
3343+ :returns: None. Raises exception if publish failed.
3344+ """
3345+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
3346+ message))
3347+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
3348+ port=port,
3349+ username=username,
3350+ password=password)
3351+
3352+ # NOTE(beisner): extra debug here re: pika hang potential:
3353+ # https://github.com/pika/pika/issues/297
3354+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
3355+ self.log.debug('Defining channel...')
3356+ channel = connection.channel()
3357+ self.log.debug('Declaring queue...')
3358+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
3359+ self.log.debug('Publishing message...')
3360+ channel.basic_publish(exchange='', routing_key=queue, body=message)
3361+ self.log.debug('Closing channel...')
3362+ channel.close()
3363+ self.log.debug('Closing connection...')
3364+ connection.close()
3365+
3366+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
3367+ username="testuser1",
3368+ password="changeme",
3369+ ssl=False, port=None):
3370+ """Get an amqp message from a rmq juju unit.
3371+
3372+ :param sentry_unit: sentry unit pointer
3373+ :param queue: message queue, default to test
3374+ :param username: amqp user name, default to testuser1
3375+ :param password: amqp user password
3376+ :param ssl: boolean, default to False
3377+ :param port: amqp port, use defaults if None
3378+ :returns: amqp message body as string. Raise if get fails.
3379+ """
3380+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
3381+ port=port,
3382+ username=username,
3383+ password=password)
3384+ channel = connection.channel()
3385+ method_frame, _, body = channel.basic_get(queue)
3386+
3387+ if method_frame:
3388+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
3389+ body))
3390+ channel.basic_ack(method_frame.delivery_tag)
3391+ channel.close()
3392+ connection.close()
3393+ return body
3394+ else:
3395+ msg = 'No message retrieved.'
3396+ amulet.raise_status(amulet.FAIL, msg)
3397
3398=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
3399--- hooks/charmhelpers/contrib/openstack/context.py 2014-06-11 09:44:51 +0000
3400+++ hooks/charmhelpers/contrib/openstack/context.py 2015-11-12 11:46:11 +0000
3401@@ -1,48 +1,94 @@
3402+# Copyright 2014-2015 Canonical Limited.
3403+#
3404+# This file is part of charm-helpers.
3405+#
3406+# charm-helpers is free software: you can redistribute it and/or modify
3407+# it under the terms of the GNU Lesser General Public License version 3 as
3408+# published by the Free Software Foundation.
3409+#
3410+# charm-helpers is distributed in the hope that it will be useful,
3411+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3412+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3413+# GNU Lesser General Public License for more details.
3414+#
3415+# You should have received a copy of the GNU Lesser General Public License
3416+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3417+
3418+import glob
3419 import json
3420 import os
3421+import re
3422 import time
3423-
3424 from base64 import b64decode
3425-
3426-from subprocess import (
3427- check_call
3428-)
3429-
3430+from subprocess import check_call
3431+
3432+import six
3433+import yaml
3434
3435 from charmhelpers.fetch import (
3436 apt_install,
3437 filter_installed_packages,
3438 )
3439-
3440 from charmhelpers.core.hookenv import (
3441 config,
3442+ is_relation_made,
3443 local_unit,
3444 log,
3445 relation_get,
3446 relation_ids,
3447 related_units,
3448+ relation_set,
3449 unit_get,
3450 unit_private_ip,
3451+ charm_name,
3452+ DEBUG,
3453+ INFO,
3454+ WARNING,
3455 ERROR,
3456 )
3457
3458+from charmhelpers.core.sysctl import create as sysctl_create
3459+from charmhelpers.core.strutils import bool_from_string
3460+
3461+from charmhelpers.core.host import (
3462+ get_bond_master,
3463+ is_phy_iface,
3464+ list_nics,
3465+ get_nic_hwaddr,
3466+ mkdir,
3467+ write_file,
3468+)
3469 from charmhelpers.contrib.hahelpers.cluster import (
3470 determine_apache_port,
3471 determine_api_port,
3472 https,
3473- is_clustered
3474+ is_clustered,
3475 )
3476-
3477 from charmhelpers.contrib.hahelpers.apache import (
3478 get_cert,
3479 get_ca_cert,
3480+ install_ca_cert,
3481 )
3482-
3483 from charmhelpers.contrib.openstack.neutron import (
3484 neutron_plugin_attribute,
3485-)
3486-
3487+ parse_data_port_mappings,
3488+)
3489+from charmhelpers.contrib.openstack.ip import (
3490+ resolve_address,
3491+ INTERNAL,
3492+)
3493+from charmhelpers.contrib.network.ip import (
3494+ get_address_in_network,
3495+ get_ipv4_addr,
3496+ get_ipv6_addr,
3497+ get_netmask_for_address,
3498+ format_ipv6_addr,
3499+ is_address_in_network,
3500+ is_bridge_member,
3501+)
3502+from charmhelpers.contrib.openstack.utils import get_host_ip
3503 CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
3504+ADDRESS_TYPES = ['admin', 'internal', 'public']
3505
3506
3507 class OSContextError(Exception):
3508@@ -50,7 +96,7 @@
3509
3510
3511 def ensure_packages(packages):
3512- '''Install but do not upgrade required plugin packages'''
3513+ """Install but do not upgrade required plugin packages."""
3514 required = filter_installed_packages(packages)
3515 if required:
3516 apt_install(required, fatal=True)
3517@@ -58,20 +104,62 @@
3518
3519 def context_complete(ctxt):
3520 _missing = []
3521- for k, v in ctxt.iteritems():
3522+ for k, v in six.iteritems(ctxt):
3523 if v is None or v == '':
3524 _missing.append(k)
3525+
3526 if _missing:
3527- log('Missing required data: %s' % ' '.join(_missing), level='INFO')
3528+ log('Missing required data: %s' % ' '.join(_missing), level=INFO)
3529 return False
3530+
3531 return True
3532
3533
3534 def config_flags_parser(config_flags):
3535+ """Parses config flags string into dict.
3536+
3537+ This parsing method supports a few different formats for the config
3538+ flag values to be parsed:
3539+
3540+ 1. A string in the simple format of key=value pairs, with the possibility
3541+ of specifying multiple key value pairs within the same string. For
3542+ example, a string in the format of 'key1=value1, key2=value2' will
3543+ return a dict of:
3544+
3545+ {'key1': 'value1',
3546+ 'key2': 'value2'}.
3547+
3548+ 2. A string in the above format, but supporting a comma-delimited list
3549+ of values for the same key. For example, a string in the format of
3550+ 'key1=value1, key2=value3,value4,value5' will return a dict of:
3551+
3552+ {'key1', 'value1',
3553+ 'key2', 'value2,value3,value4'}
3554+
3555+ 3. A string containing a colon character (:) prior to an equal
3556+ character (=) will be treated as yaml and parsed as such. This can be
3557+ used to specify more complex key value pairs. For example,
3558+ a string in the format of 'key1: subkey1=value1, subkey2=value2' will
3559+ return a dict of:
3560+
3561+ {'key1', 'subkey1=value1, subkey2=value2'}
3562+
3563+ The provided config_flags string may be a list of comma-separated values
3564+ which themselves may be comma-separated list of values.
3565+ """
3566+ # If we find a colon before an equals sign then treat it as yaml.
3567+ # Note: limit it to finding the colon first since this indicates assignment
3568+ # for inline yaml.
3569+ colon = config_flags.find(':')
3570+ equals = config_flags.find('=')
3571+ if colon > 0:
3572+ if colon < equals or equals < 0:
3573+ return yaml.safe_load(config_flags)
3574+
3575 if config_flags.find('==') >= 0:
3576- log("config_flags is not in expected format (key=value)",
3577- level=ERROR)
3578+ log("config_flags is not in expected format (key=value)", level=ERROR)
3579 raise OSContextError
3580+
3581 # strip the following from each value.
3582 post_strippers = ' ,'
3583 # we strip any leading/trailing '=' or ' ' from the string then
3584@@ -79,7 +167,7 @@
3585 split = config_flags.strip(' =').split('=')
3586 limit = len(split)
3587 flags = {}
3588- for i in xrange(0, limit - 1):
3589+ for i in range(0, limit - 1):
3590 current = split[i]
3591 next = split[i + 1]
3592 vindex = next.rfind(',')
3593@@ -94,63 +182,125 @@
3594 # if this not the first entry, expect an embedded key.
3595 index = current.rfind(',')
3596 if index < 0:
3597- log("invalid config value(s) at index %s" % (i),
3598- level=ERROR)
3599+ log("Invalid config value(s) at index %s" % (i), level=ERROR)
3600 raise OSContextError
3601 key = current[index + 1:]
3602
3603 # Add to collection.
3604 flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
3605+
3606 return flags
3607
3608
3609 class OSContextGenerator(object):
3610+ """Base class for all context generators."""
3611 interfaces = []
3612+ related = False
3613+ complete = False
3614+ missing_data = []
3615
3616 def __call__(self):
3617 raise NotImplementedError
3618
3619+ def context_complete(self, ctxt):
3620+ """Check for missing data for the required context data.
3621+ Set self.missing_data if it exists and return False.
3622+ Set self.complete if no missing data and return True.
3623+ """
3624+ # Fresh start
3625+ self.complete = False
3626+ self.missing_data = []
3627+ for k, v in six.iteritems(ctxt):
3628+ if v is None or v == '':
3629+ if k not in self.missing_data:
3630+ self.missing_data.append(k)
3631+
3632+ if self.missing_data:
3633+ self.complete = False
3634+ log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
3635+ else:
3636+ self.complete = True
3637+ return self.complete
3638+
3639+ def get_related(self):
3640+ """Check if any of the context interfaces have relation ids.
3641+ Set self.related and return True if one of the interfaces
3642+ has relation ids.
3643+ """
3644+ # Fresh start
3645+ self.related = False
3646+ try:
3647+ for interface in self.interfaces:
3648+ if relation_ids(interface):
3649+ self.related = True
3650+ return self.related
3651+ except AttributeError as e:
3652+ log("{} {}"
3653+ "".format(self, e), 'INFO')
3654+ return self.related
3655+
3656
3657 class SharedDBContext(OSContextGenerator):
3658 interfaces = ['shared-db']
3659
3660 def __init__(self,
3661 database=None, user=None, relation_prefix=None, ssl_dir=None):
3662- '''
3663- Allows inspecting relation for settings prefixed with relation_prefix.
3664- This is useful for parsing access for multiple databases returned via
3665- the shared-db interface (eg, nova_password, quantum_password)
3666- '''
3667+ """Allows inspecting relation for settings prefixed with
3668+ relation_prefix. This is useful for parsing access for multiple
3669+ databases returned via the shared-db interface (eg, nova_password,
3670+ quantum_password)
3671+ """
3672 self.relation_prefix = relation_prefix
3673 self.database = database
3674 self.user = user
3675 self.ssl_dir = ssl_dir
3676+ self.rel_name = self.interfaces[0]
3677
3678 def __call__(self):
3679 self.database = self.database or config('database')
3680 self.user = self.user or config('database-user')
3681 if None in [self.database, self.user]:
3682- log('Could not generate shared_db context. '
3683- 'Missing required charm config options. '
3684- '(database name and user)')
3685+ log("Could not generate shared_db context. Missing required charm "
3686+ "config options. (database name and user)", level=ERROR)
3687 raise OSContextError
3688+
3689 ctxt = {}
3690
3691+ # NOTE(jamespage) if mysql charm provides a network upon which
3692+ # access to the database should be made, reconfigure relation
3693+ # with the service units local address and defer execution
3694+ access_network = relation_get('access-network')
3695+ if access_network is not None:
3696+ if self.relation_prefix is not None:
3697+ hostname_key = "{}_hostname".format(self.relation_prefix)
3698+ else:
3699+ hostname_key = "hostname"
3700+ access_hostname = get_address_in_network(access_network,
3701+ unit_get('private-address'))
3702+ set_hostname = relation_get(attribute=hostname_key,
3703+ unit=local_unit())
3704+ if set_hostname != access_hostname:
3705+ relation_set(relation_settings={hostname_key: access_hostname})
3706+ return None # Defer any further hook execution for now....
3707+
3708 password_setting = 'password'
3709 if self.relation_prefix:
3710 password_setting = self.relation_prefix + '_password'
3711
3712- for rid in relation_ids('shared-db'):
3713+ for rid in relation_ids(self.interfaces[0]):
3714+ self.related = True
3715 for unit in related_units(rid):
3716 rdata = relation_get(rid=rid, unit=unit)
3717+ host = rdata.get('db_host')
3718+ host = format_ipv6_addr(host) or host
3719 ctxt = {
3720- 'database_host': rdata.get('db_host'),
3721+ 'database_host': host,
3722 'database': self.database,
3723 'database_user': self.user,
3724 'database_password': rdata.get(password_setting),
3725 'database_type': 'mysql'
3726 }
3727- if context_complete(ctxt):
3728+ if self.context_complete(ctxt):
3729 db_ssl(rdata, ctxt, self.ssl_dir)
3730 return ctxt
3731 return {}
3732@@ -165,23 +315,25 @@
3733 def __call__(self):
3734 self.database = self.database or config('database')
3735 if self.database is None:
3736- log('Could not generate postgresql_db context. '
3737- 'Missing required charm config options. '
3738- '(database name)')
3739+ log('Could not generate postgresql_db context. Missing required '
3740+ 'charm config options. (database name)', level=ERROR)
3741 raise OSContextError
3742+
3743 ctxt = {}
3744-
3745 for rid in relation_ids(self.interfaces[0]):
3746+ self.related = True
3747 for unit in related_units(rid):
3748- ctxt = {
3749- 'database_host': relation_get('host', rid=rid, unit=unit),
3750- 'database': self.database,
3751- 'database_user': relation_get('user', rid=rid, unit=unit),
3752- 'database_password': relation_get('password', rid=rid, unit=unit),
3753- 'database_type': 'postgresql',
3754- }
3755- if context_complete(ctxt):
3756+ rel_host = relation_get('host', rid=rid, unit=unit)
3757+ rel_user = relation_get('user', rid=rid, unit=unit)
3758+ rel_passwd = relation_get('password', rid=rid, unit=unit)
3759+ ctxt = {'database_host': rel_host,
3760+ 'database': self.database,
3761+ 'database_user': rel_user,
3762+ 'database_password': rel_passwd,
3763+ 'database_type': 'postgresql'}
3764+ if self.context_complete(ctxt):
3765 return ctxt
3766+
3767 return {}
3768
3769
3770@@ -190,85 +342,126 @@
3771 ca_path = os.path.join(ssl_dir, 'db-client.ca')
3772 with open(ca_path, 'w') as fh:
3773 fh.write(b64decode(rdata['ssl_ca']))
3774+
3775 ctxt['database_ssl_ca'] = ca_path
3776 elif 'ssl_ca' in rdata:
3777- log("Charm not setup for ssl support but ssl ca found")
3778+ log("Charm not setup for ssl support but ssl ca found", level=INFO)
3779 return ctxt
3780+
3781 if 'ssl_cert' in rdata:
3782 cert_path = os.path.join(
3783 ssl_dir, 'db-client.cert')
3784 if not os.path.exists(cert_path):
3785- log("Waiting 1m for ssl client cert validity")
3786+ log("Waiting 1m for ssl client cert validity", level=INFO)
3787 time.sleep(60)
3788+
3789 with open(cert_path, 'w') as fh:
3790 fh.write(b64decode(rdata['ssl_cert']))
3791+
3792 ctxt['database_ssl_cert'] = cert_path
3793 key_path = os.path.join(ssl_dir, 'db-client.key')
3794 with open(key_path, 'w') as fh:
3795 fh.write(b64decode(rdata['ssl_key']))
3796+
3797 ctxt['database_ssl_key'] = key_path
3798+
3799 return ctxt
3800
3801
3802 class IdentityServiceContext(OSContextGenerator):
3803- interfaces = ['identity-service']
3804+
3805+ def __init__(self, service=None, service_user=None, rel_name='identity-service'):
3806+ self.service = service
3807+ self.service_user = service_user
3808+ self.rel_name = rel_name
3809+ self.interfaces = [self.rel_name]
3810
3811 def __call__(self):
3812- log('Generating template context for identity-service')
3813+ log('Generating template context for ' + self.rel_name, level=DEBUG)
3814 ctxt = {}
3815
3816- for rid in relation_ids('identity-service'):
3817+ if self.service and self.service_user:
3818+ # This is required for pki token signing if we don't want /tmp to
3819+ # be used.
3820+ cachedir = '/var/cache/%s' % (self.service)
3821+ if not os.path.isdir(cachedir):
3822+ log("Creating service cache dir %s" % (cachedir), level=DEBUG)
3823+ mkdir(path=cachedir, owner=self.service_user,
3824+ group=self.service_user, perms=0o700)
3825+
3826+ ctxt['signing_dir'] = cachedir
3827+
3828+ for rid in relation_ids(self.rel_name):
3829+ self.related = True
3830 for unit in related_units(rid):
3831 rdata = relation_get(rid=rid, unit=unit)
3832- ctxt = {
3833- 'service_port': rdata.get('service_port'),
3834- 'service_host': rdata.get('service_host'),
3835- 'auth_host': rdata.get('auth_host'),
3836- 'auth_port': rdata.get('auth_port'),
3837- 'admin_tenant_name': rdata.get('service_tenant'),
3838- 'admin_user': rdata.get('service_username'),
3839- 'admin_password': rdata.get('service_password'),
3840- 'service_protocol':
3841- rdata.get('service_protocol') or 'http',
3842- 'auth_protocol':
3843- rdata.get('auth_protocol') or 'http',
3844- }
3845- if context_complete(ctxt):
3846+ serv_host = rdata.get('service_host')
3847+ serv_host = format_ipv6_addr(serv_host) or serv_host
3848+ auth_host = rdata.get('auth_host')
3849+ auth_host = format_ipv6_addr(auth_host) or auth_host
3850+ svc_protocol = rdata.get('service_protocol') or 'http'
3851+ auth_protocol = rdata.get('auth_protocol') or 'http'
3852+ ctxt.update({'service_port': rdata.get('service_port'),
3853+ 'service_host': serv_host,
3854+ 'auth_host': auth_host,
3855+ 'auth_port': rdata.get('auth_port'),
3856+ 'admin_tenant_name': rdata.get('service_tenant'),
3857+ 'admin_user': rdata.get('service_username'),
3858+ 'admin_password': rdata.get('service_password'),
3859+ 'service_protocol': svc_protocol,
3860+ 'auth_protocol': auth_protocol})
3861+
3862+ if self.context_complete(ctxt):
3863 # NOTE(jamespage) this is required for >= icehouse
3864 # so a missing value just indicates keystone needs
3865 # upgrading
3866 ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
3867 return ctxt
3868+
3869 return {}
3870
3871
3872 class AMQPContext(OSContextGenerator):
3873- interfaces = ['amqp']
3874
3875- def __init__(self, ssl_dir=None):
3876+ def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
3877 self.ssl_dir = ssl_dir
3878+ self.rel_name = rel_name
3879+ self.relation_prefix = relation_prefix
3880+ self.interfaces = [rel_name]
3881
3882 def __call__(self):
3883- log('Generating template context for amqp')
3884+ log('Generating template context for amqp', level=DEBUG)
3885 conf = config()
3886+ if self.relation_prefix:
3887+ user_setting = '%s-rabbit-user' % (self.relation_prefix)
3888+ vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
3889+ else:
3890+ user_setting = 'rabbit-user'
3891+ vhost_setting = 'rabbit-vhost'
3892+
3893 try:
3894- username = conf['rabbit-user']
3895- vhost = conf['rabbit-vhost']
3896+ username = conf[user_setting]
3897+ vhost = conf[vhost_setting]
3898 except KeyError as e:
3899- log('Could not generate shared_db context. '
3900- 'Missing required charm config options: %s.' % e)
3901+ log('Could not generate shared_db context. Missing required charm '
3902+ 'config options: %s.' % e, level=ERROR)
3903 raise OSContextError
3904+
3905 ctxt = {}
3906- for rid in relation_ids('amqp'):
3907+ for rid in relation_ids(self.rel_name):
3908 ha_vip_only = False
3909+ self.related = True
3910 for unit in related_units(rid):
3911 if relation_get('clustered', rid=rid, unit=unit):
3912 ctxt['clustered'] = True
3913- ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
3914- unit=unit)
3915+ vip = relation_get('vip', rid=rid, unit=unit)
3916+ vip = format_ipv6_addr(vip) or vip
3917+ ctxt['rabbitmq_host'] = vip
3918 else:
3919- ctxt['rabbitmq_host'] = relation_get('private-address',
3920- rid=rid, unit=unit)
3921+ host = relation_get('private-address', rid=rid, unit=unit)
3922+ host = format_ipv6_addr(host) or host
3923+ ctxt['rabbitmq_host'] = host
3924+
3925 ctxt.update({
3926 'rabbitmq_user': username,
3927 'rabbitmq_password': relation_get('password', rid=rid,
3928@@ -279,6 +472,7 @@
3929 ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
3930 if ssl_port:
3931 ctxt['rabbit_ssl_port'] = ssl_port
3932+
3933 ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
3934 if ssl_ca:
3935 ctxt['rabbit_ssl_ca'] = ssl_ca
3936@@ -289,104 +483,172 @@
3937 ha_vip_only = relation_get('ha-vip-only',
3938 rid=rid, unit=unit) is not None
3939
3940- if context_complete(ctxt):
3941+ if self.context_complete(ctxt):
3942 if 'rabbit_ssl_ca' in ctxt:
3943 if not self.ssl_dir:
3944- log(("Charm not setup for ssl support "
3945- "but ssl ca found"))
3946+ log("Charm not setup for ssl support but ssl ca "
3947+ "found", level=INFO)
3948 break
3949+
3950 ca_path = os.path.join(
3951 self.ssl_dir, 'rabbit-client-ca.pem')
3952 with open(ca_path, 'w') as fh:
3953 fh.write(b64decode(ctxt['rabbit_ssl_ca']))
3954 ctxt['rabbit_ssl_ca'] = ca_path
3955+
3956 # Sufficient information found = break out!
3957 break
3958+
3959 # Used for active/active rabbitmq >= grizzly
3960- if ('clustered' not in ctxt or ha_vip_only) \
3961- and len(related_units(rid)) > 1:
3962+ if (('clustered' not in ctxt or ha_vip_only) and
3963+ len(related_units(rid)) > 1):
3964 rabbitmq_hosts = []
3965 for unit in related_units(rid):
3966- rabbitmq_hosts.append(relation_get('private-address',
3967- rid=rid, unit=unit))
3968- ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
3969- if not context_complete(ctxt):
3970+ host = relation_get('private-address', rid=rid, unit=unit)
3971+ host = format_ipv6_addr(host) or host
3972+ rabbitmq_hosts.append(host)
3973+
3974+ ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
3975+
3976+ oslo_messaging_flags = conf.get('oslo-messaging-flags', None)
3977+ if oslo_messaging_flags:
3978+ ctxt['oslo_messaging_flags'] = config_flags_parser(
3979+ oslo_messaging_flags)
3980+
3981+ if not self.complete:
3982 return {}
3983- else:
3984- return ctxt
3985+
3986+ return ctxt
3987
3988
3989 class CephContext(OSContextGenerator):
3990+ """Generates context for /etc/ceph/ceph.conf templates."""
3991 interfaces = ['ceph']
3992
3993 def __call__(self):
3994- '''This generates context for /etc/ceph/ceph.conf templates'''
3995 if not relation_ids('ceph'):
3996 return {}
3997
3998- log('Generating template context for ceph')
3999-
4000+ log('Generating template context for ceph', level=DEBUG)
4001 mon_hosts = []
4002- auth = None
4003- key = None
4004- use_syslog = str(config('use-syslog')).lower()
4005+ ctxt = {
4006+ 'use_syslog': str(config('use-syslog')).lower()
4007+ }
4008 for rid in relation_ids('ceph'):
4009 for unit in related_units(rid):
4010- mon_hosts.append(relation_get('private-address', rid=rid,
4011- unit=unit))
4012- auth = relation_get('auth', rid=rid, unit=unit)
4013- key = relation_get('key', rid=rid, unit=unit)
4014+ if not ctxt.get('auth'):
4015+ ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
4016+ if not ctxt.get('key'):
4017+ ctxt['key'] = relation_get('key', rid=rid, unit=unit)
4018+ ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
4019+ unit=unit)
4020+ unit_priv_addr = relation_get('private-address', rid=rid,
4021+ unit=unit)
4022+ ceph_addr = ceph_pub_addr or unit_priv_addr
4023+ ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
4024+ mon_hosts.append(ceph_addr)
4025
4026- ctxt = {
4027- 'mon_hosts': ' '.join(mon_hosts),
4028- 'auth': auth,
4029- 'key': key,
4030- 'use_syslog': use_syslog
4031- }
4032+ ctxt['mon_hosts'] = ' '.join(sorted(mon_hosts))
4033
4034 if not os.path.isdir('/etc/ceph'):
4035 os.mkdir('/etc/ceph')
4036
4037- if not context_complete(ctxt):
4038+ if not self.context_complete(ctxt):
4039 return {}
4040
4041 ensure_packages(['ceph-common'])
4042-
4043 return ctxt
4044
4045
4046 class HAProxyContext(OSContextGenerator):
4047+ """Provides half a context for the haproxy template, which describes
4048+ all peers to be included in the cluster. Each charm needs to include
4049+ its own context generator that describes the port mapping.
4050+ """
4051 interfaces = ['cluster']
4052
4053+ def __init__(self, singlenode_mode=False):
4054+ self.singlenode_mode = singlenode_mode
4055+
4056 def __call__(self):
4057- '''
4058- Builds half a context for the haproxy template, which describes
4059- all peers to be included in the cluster. Each charm needs to include
4060- its own context generator that describes the port mapping.
4061- '''
4062- if not relation_ids('cluster'):
4063+ if not relation_ids('cluster') and not self.singlenode_mode:
4064 return {}
4065
4066+ if config('prefer-ipv6'):
4067+ addr = get_ipv6_addr(exc_list=[config('vip')])[0]
4068+ else:
4069+ addr = get_host_ip(unit_get('private-address'))
4070+
4071+ l_unit = local_unit().replace('/', '-')
4072 cluster_hosts = {}
4073- l_unit = local_unit().replace('/', '-')
4074- cluster_hosts[l_unit] = unit_get('private-address')
4075-
4076+
4077+ # NOTE(jamespage): build out map of configured network endpoints
4078+ # and associated backends
4079+ for addr_type in ADDRESS_TYPES:
4080+ cfg_opt = 'os-{}-network'.format(addr_type)
4081+ laddr = get_address_in_network(config(cfg_opt))
4082+ if laddr:
4083+ netmask = get_netmask_for_address(laddr)
4084+ cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
4085+ netmask),
4086+ 'backends': {l_unit: laddr}}
4087+ for rid in relation_ids('cluster'):
4088+ for unit in related_units(rid):
4089+ _laddr = relation_get('{}-address'.format(addr_type),
4090+ rid=rid, unit=unit)
4091+ if _laddr:
4092+ _unit = unit.replace('/', '-')
4093+ cluster_hosts[laddr]['backends'][_unit] = _laddr
4094+
4095+ # NOTE(jamespage) add backend based on private address - this
4096+ # with either be the only backend or the fallback if no acls
4097+ # match in the frontend
4098+ cluster_hosts[addr] = {}
4099+ netmask = get_netmask_for_address(addr)
4100+ cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
4101+ 'backends': {l_unit: addr}}
4102 for rid in relation_ids('cluster'):
4103 for unit in related_units(rid):
4104- _unit = unit.replace('/', '-')
4105- addr = relation_get('private-address', rid=rid, unit=unit)
4106- cluster_hosts[_unit] = addr
4107+ _laddr = relation_get('private-address',
4108+ rid=rid, unit=unit)
4109+ if _laddr:
4110+ _unit = unit.replace('/', '-')
4111+ cluster_hosts[addr]['backends'][_unit] = _laddr
4112
4113 ctxt = {
4114- 'units': cluster_hosts,
4115+ 'frontends': cluster_hosts,
4116+ 'default_backend': addr
4117 }
4118- if len(cluster_hosts.keys()) > 1:
4119- # Enable haproxy when we have enough peers.
4120- log('Ensuring haproxy enabled in /etc/default/haproxy.')
4121- with open('/etc/default/haproxy', 'w') as out:
4122- out.write('ENABLED=1\n')
4123- return ctxt
4124- log('HAProxy context is incomplete, this unit has no peers.')
4125+
4126+ if config('haproxy-server-timeout'):
4127+ ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
4128+
4129+ if config('haproxy-client-timeout'):
4130+ ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
4131+
4132+ if config('prefer-ipv6'):
4133+ ctxt['ipv6'] = True
4134+ ctxt['local_host'] = 'ip6-localhost'
4135+ ctxt['haproxy_host'] = '::'
4136+ ctxt['stat_port'] = ':::8888'
4137+ else:
4138+ ctxt['local_host'] = '127.0.0.1'
4139+ ctxt['haproxy_host'] = '0.0.0.0'
4140+ ctxt['stat_port'] = ':8888'
4141+
4142+ for frontend in cluster_hosts:
4143+ if (len(cluster_hosts[frontend]['backends']) > 1 or
4144+ self.singlenode_mode):
4145+ # Enable haproxy when we have enough peers.
4146+ log('Ensuring haproxy enabled in /etc/default/haproxy.',
4147+ level=DEBUG)
4148+ with open('/etc/default/haproxy', 'w') as out:
4149+ out.write('ENABLED=1\n')
4150+
4151+ return ctxt
4152+
4153+ log('HAProxy context is incomplete, this unit has no peers.',
4154+ level=INFO)
4155 return {}
4156
4157
4158@@ -394,36 +656,36 @@
4159 interfaces = ['image-service']
4160
4161 def __call__(self):
4162- '''
4163- Obtains the glance API server from the image-service relation. Useful
4164- in nova and cinder (currently).
4165- '''
4166- log('Generating template context for image-service.')
4167+ """Obtains the glance API server from the image-service relation.
4168+ Useful in nova and cinder (currently).
4169+ """
4170+ log('Generating template context for image-service.', level=DEBUG)
4171 rids = relation_ids('image-service')
4172 if not rids:
4173 return {}
4174+
4175 for rid in rids:
4176 for unit in related_units(rid):
4177 api_server = relation_get('glance-api-server',
4178 rid=rid, unit=unit)
4179 if api_server:
4180 return {'glance_api_servers': api_server}
4181- log('ImageService context is incomplete. '
4182- 'Missing required relation data.')
4183+
4184+ log("ImageService context is incomplete. Missing required relation "
4185+ "data.", level=INFO)
4186 return {}
4187
4188
4189 class ApacheSSLContext(OSContextGenerator):
4190-
4191- """
4192- Generates a context for an apache vhost configuration that configures
4193+ """Generates a context for an apache vhost configuration that configures
4194 HTTPS reverse proxying for one or many endpoints. Generated context
4195- looks something like:
4196- {
4197- 'namespace': 'cinder',
4198- 'private_address': 'iscsi.mycinderhost.com',
4199- 'endpoints': [(8776, 8766), (8777, 8767)]
4200- }
4201+ looks something like::
4202+
4203+ {
4204+ 'namespace': 'cinder',
4205+ 'private_address': 'iscsi.mycinderhost.com',
4206+ 'endpoints': [(8776, 8766), (8777, 8767)]
4207+ }
4208
4209 The endpoints list consists of a tuples mapping external ports
4210 to internal ports.
4211@@ -439,44 +701,119 @@
4212 cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
4213 check_call(cmd)
4214
4215- def configure_cert(self):
4216- if not os.path.isdir('/etc/apache2/ssl'):
4217- os.mkdir('/etc/apache2/ssl')
4218+ def configure_cert(self, cn=None):
4219 ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
4220- if not os.path.isdir(ssl_dir):
4221- os.mkdir(ssl_dir)
4222- cert, key = get_cert()
4223- with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
4224- cert_out.write(b64decode(cert))
4225- with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
4226- key_out.write(b64decode(key))
4227+ mkdir(path=ssl_dir)
4228+ cert, key = get_cert(cn)
4229+ if cn:
4230+ cert_filename = 'cert_{}'.format(cn)
4231+ key_filename = 'key_{}'.format(cn)
4232+ else:
4233+ cert_filename = 'cert'
4234+ key_filename = 'key'
4235+
4236+ write_file(path=os.path.join(ssl_dir, cert_filename),
4237+ content=b64decode(cert))
4238+ write_file(path=os.path.join(ssl_dir, key_filename),
4239+ content=b64decode(key))
4240+
4241+ def configure_ca(self):
4242 ca_cert = get_ca_cert()
4243 if ca_cert:
4244- with open(CA_CERT_PATH, 'w') as ca_out:
4245- ca_out.write(b64decode(ca_cert))
4246- check_call(['update-ca-certificates'])
4247+ install_ca_cert(b64decode(ca_cert))
4248+
4249+ def canonical_names(self):
4250+ """Figure out which canonical names clients will access this service.
4251+ """
4252+ cns = []
4253+ for r_id in relation_ids('identity-service'):
4254+ for unit in related_units(r_id):
4255+ rdata = relation_get(rid=r_id, unit=unit)
4256+ for k in rdata:
4257+ if k.startswith('ssl_key_'):
4258+ cns.append(k.lstrip('ssl_key_'))
4259+
4260+ return sorted(list(set(cns)))
4261+
4262+ def get_network_addresses(self):
4263+ """For each network configured, return corresponding address and vip
4264+ (if available).
4265+
4266+ Returns a list of tuples of the form:
4267+
4268+ [(address_in_net_a, vip_in_net_a),
4269+ (address_in_net_b, vip_in_net_b),
4270+ ...]
4271+
4272+ or, if no vip(s) available:
4273+
4274+ [(address_in_net_a, address_in_net_a),
4275+ (address_in_net_b, address_in_net_b),
4276+ ...]
4277+ """
4278+ addresses = []
4279+ if config('vip'):
4280+ vips = config('vip').split()
4281+ else:
4282+ vips = []
4283+
4284+ for net_type in ['os-internal-network', 'os-admin-network',
4285+ 'os-public-network']:
4286+ addr = get_address_in_network(config(net_type),
4287+ unit_get('private-address'))
4288+ if len(vips) > 1 and is_clustered():
4289+ if not config(net_type):
4290+ log("Multiple networks configured but net_type "
4291+ "is None (%s)." % net_type, level=WARNING)
4292+ continue
4293+
4294+ for vip in vips:
4295+ if is_address_in_network(config(net_type), vip):
4296+ addresses.append((addr, vip))
4297+ break
4298+
4299+ elif is_clustered() and config('vip'):
4300+ addresses.append((addr, config('vip')))
4301+ else:
4302+ addresses.append((addr, addr))
4303+
4304+ return sorted(addresses)
4305
4306 def __call__(self):
4307- if isinstance(self.external_ports, basestring):
4308+ if isinstance(self.external_ports, six.string_types):
4309 self.external_ports = [self.external_ports]
4310- if (not self.external_ports or not https()):
4311+
4312+ if not self.external_ports or not https():
4313 return {}
4314
4315- self.configure_cert()
4316+ self.configure_ca()
4317 self.enable_modules()
4318
4319- ctxt = {
4320- 'namespace': self.service_namespace,
4321- 'private_address': unit_get('private-address'),
4322- 'endpoints': []
4323- }
4324- if is_clustered():
4325- ctxt['private_address'] = config('vip')
4326- for api_port in self.external_ports:
4327- ext_port = determine_apache_port(api_port)
4328- int_port = determine_api_port(api_port)
4329- portmap = (int(ext_port), int(int_port))
4330- ctxt['endpoints'].append(portmap)
4331+ ctxt = {'namespace': self.service_namespace,
4332+ 'endpoints': [],
4333+ 'ext_ports': []}
4334+
4335+ cns = self.canonical_names()
4336+ if cns:
4337+ for cn in cns:
4338+ self.configure_cert(cn)
4339+ else:
4340+ # Expect cert/key provided in config (currently assumed that ca
4341+ # uses ip for cn)
4342+ cn = resolve_address(endpoint_type=INTERNAL)
4343+ self.configure_cert(cn)
4344+
4345+ addresses = self.get_network_addresses()
4346+ for address, endpoint in sorted(set(addresses)):
4347+ for api_port in self.external_ports:
4348+ ext_port = determine_apache_port(api_port,
4349+ singlenode_mode=True)
4350+ int_port = determine_api_port(api_port, singlenode_mode=True)
4351+ portmap = (address, endpoint, int(ext_port), int(int_port))
4352+ ctxt['endpoints'].append(portmap)
4353+ ctxt['ext_ports'].append(int(ext_port))
4354+
4355+ ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
4356 return ctxt
4357
4358
4359@@ -493,21 +830,23 @@
4360
4361 @property
4362 def packages(self):
4363- return neutron_plugin_attribute(
4364- self.plugin, 'packages', self.network_manager)
4365+ return neutron_plugin_attribute(self.plugin, 'packages',
4366+ self.network_manager)
4367
4368 @property
4369 def neutron_security_groups(self):
4370 return None
4371
4372 def _ensure_packages(self):
4373- [ensure_packages(pkgs) for pkgs in self.packages]
4374+ for pkgs in self.packages:
4375+ ensure_packages(pkgs)
4376
4377 def _save_flag_file(self):
4378 if self.network_manager == 'quantum':
4379 _file = '/etc/nova/quantum_plugin.conf'
4380 else:
4381 _file = '/etc/nova/neutron_plugin.conf'
4382+
4383 with open(_file, 'wb') as out:
4384 out.write(self.plugin + '\n')
4385
4386@@ -516,50 +855,104 @@
4387 self.network_manager)
4388 config = neutron_plugin_attribute(self.plugin, 'config',
4389 self.network_manager)
4390- ovs_ctxt = {
4391- 'core_plugin': driver,
4392- 'neutron_plugin': 'ovs',
4393- 'neutron_security_groups': self.neutron_security_groups,
4394- 'local_ip': unit_private_ip(),
4395- 'config': config
4396- }
4397+ ovs_ctxt = {'core_plugin': driver,
4398+ 'neutron_plugin': 'ovs',
4399+ 'neutron_security_groups': self.neutron_security_groups,
4400+ 'local_ip': unit_private_ip(),
4401+ 'config': config}
4402
4403 return ovs_ctxt
4404
4405+ def nuage_ctxt(self):
4406+ driver = neutron_plugin_attribute(self.plugin, 'driver',
4407+ self.network_manager)
4408+ config = neutron_plugin_attribute(self.plugin, 'config',
4409+ self.network_manager)
4410+ nuage_ctxt = {'core_plugin': driver,
4411+ 'neutron_plugin': 'vsp',
4412+ 'neutron_security_groups': self.neutron_security_groups,
4413+ 'local_ip': unit_private_ip(),
4414+ 'config': config}
4415+
4416+ return nuage_ctxt
4417+
4418 def nvp_ctxt(self):
4419 driver = neutron_plugin_attribute(self.plugin, 'driver',
4420 self.network_manager)
4421 config = neutron_plugin_attribute(self.plugin, 'config',
4422 self.network_manager)
4423- nvp_ctxt = {
4424- 'core_plugin': driver,
4425- 'neutron_plugin': 'nvp',
4426- 'neutron_security_groups': self.neutron_security_groups,
4427- 'local_ip': unit_private_ip(),
4428- 'config': config
4429- }
4430+ nvp_ctxt = {'core_plugin': driver,
4431+ 'neutron_plugin': 'nvp',
4432+ 'neutron_security_groups': self.neutron_security_groups,
4433+ 'local_ip': unit_private_ip(),
4434+ 'config': config}
4435
4436 return nvp_ctxt
4437
4438+ def n1kv_ctxt(self):
4439+ driver = neutron_plugin_attribute(self.plugin, 'driver',
4440+ self.network_manager)
4441+ n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
4442+ self.network_manager)
4443+ n1kv_user_config_flags = config('n1kv-config-flags')
4444+ restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
4445+ n1kv_ctxt = {'core_plugin': driver,
4446+ 'neutron_plugin': 'n1kv',
4447+ 'neutron_security_groups': self.neutron_security_groups,
4448+ 'local_ip': unit_private_ip(),
4449+ 'config': n1kv_config,
4450+ 'vsm_ip': config('n1kv-vsm-ip'),
4451+ 'vsm_username': config('n1kv-vsm-username'),
4452+ 'vsm_password': config('n1kv-vsm-password'),
4453+ 'restrict_policy_profiles': restrict_policy_profiles}
4454+
4455+ if n1kv_user_config_flags:
4456+ flags = config_flags_parser(n1kv_user_config_flags)
4457+ n1kv_ctxt['user_config_flags'] = flags
4458+
4459+ return n1kv_ctxt
4460+
4461+ def calico_ctxt(self):
4462+ driver = neutron_plugin_attribute(self.plugin, 'driver',
4463+ self.network_manager)
4464+ config = neutron_plugin_attribute(self.plugin, 'config',
4465+ self.network_manager)
4466+ calico_ctxt = {'core_plugin': driver,
4467+ 'neutron_plugin': 'Calico',
4468+ 'neutron_security_groups': self.neutron_security_groups,
4469+ 'local_ip': unit_private_ip(),
4470+ 'config': config}
4471+
4472+ return calico_ctxt
4473+
4474 def neutron_ctxt(self):
4475 if https():
4476 proto = 'https'
4477 else:
4478 proto = 'http'
4479+
4480 if is_clustered():
4481 host = config('vip')
4482 else:
4483 host = unit_get('private-address')
4484- url = '%s://%s:%s' % (proto, host, '9696')
4485- ctxt = {
4486- 'network_manager': self.network_manager,
4487- 'neutron_url': url,
4488- }
4489+
4490+ ctxt = {'network_manager': self.network_manager,
4491+ 'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
4492 return ctxt
4493
4494+ def pg_ctxt(self):
4495+ driver = neutron_plugin_attribute(self.plugin, 'driver',
4496+ self.network_manager)
4497+ config = neutron_plugin_attribute(self.plugin, 'config',
4498+ self.network_manager)
4499+ ovs_ctxt = {'core_plugin': driver,
4500+ 'neutron_plugin': 'plumgrid',
4501+ 'neutron_security_groups': self.neutron_security_groups,
4502+ 'local_ip': unit_private_ip(),
4503+ 'config': config}
4504+ return ovs_ctxt
4505+
4506 def __call__(self):
4507- self._ensure_packages()
4508-
4509 if self.network_manager not in ['quantum', 'neutron']:
4510 return {}
4511
4512@@ -570,8 +963,16 @@
4513
4514 if self.plugin == 'ovs':
4515 ctxt.update(self.ovs_ctxt())
4516- elif self.plugin == 'nvp':
4517+ elif self.plugin in ['nvp', 'nsx']:
4518 ctxt.update(self.nvp_ctxt())
4519+ elif self.plugin == 'n1kv':
4520+ ctxt.update(self.n1kv_ctxt())
4521+ elif self.plugin == 'Calico':
4522+ ctxt.update(self.calico_ctxt())
4523+ elif self.plugin == 'vsp':
4524+ ctxt.update(self.nuage_ctxt())
4525+ elif self.plugin == 'plumgrid':
4526+ ctxt.update(self.pg_ctxt())
4527
4528 alchemy_flags = config('neutron-alchemy-flags')
4529 if alchemy_flags:
4530@@ -582,24 +983,94 @@
4531 return ctxt
4532
4533
4534+class NeutronPortContext(OSContextGenerator):
4535+
4536+ def resolve_ports(self, ports):
4537+ """Resolve NICs not yet bound to bridge(s)
4538+
4539+ If hwaddress provided then returns resolved hwaddress otherwise NIC.
4540+ """
4541+ if not ports:
4542+ return None
4543+
4544+ hwaddr_to_nic = {}
4545+ hwaddr_to_ip = {}
4546+ for nic in list_nics():
4547+ # Ignore virtual interfaces (bond masters will be identified from
4548+ # their slaves)
4549+ if not is_phy_iface(nic):
4550+ continue
4551+
4552+ _nic = get_bond_master(nic)
4553+ if _nic:
4554+ log("Replacing iface '%s' with bond master '%s'" % (nic, _nic),
4555+ level=DEBUG)
4556+ nic = _nic
4557+
4558+ hwaddr = get_nic_hwaddr(nic)
4559+ hwaddr_to_nic[hwaddr] = nic
4560+ addresses = get_ipv4_addr(nic, fatal=False)
4561+ addresses += get_ipv6_addr(iface=nic, fatal=False)
4562+ hwaddr_to_ip[hwaddr] = addresses
4563+
4564+ resolved = []
4565+ mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
4566+ for entry in ports:
4567+ if re.match(mac_regex, entry):
4568+ # NIC is in known NICs and does NOT hace an IP address
4569+ if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
4570+ # If the nic is part of a bridge then don't use it
4571+ if is_bridge_member(hwaddr_to_nic[entry]):
4572+ continue
4573+
4574+ # Entry is a MAC address for a valid interface that doesn't
4575+ # have an IP address assigned yet.
4576+ resolved.append(hwaddr_to_nic[entry])
4577+ else:
4578+ # If the passed entry is not a MAC address, assume it's a valid
4579+ # interface, and that the user put it there on purpose (we can
4580+ # trust it to be the real external network).
4581+ resolved.append(entry)
4582+
4583+ # Ensure no duplicates
4584+ return list(set(resolved))
4585+
4586+
4587 class OSConfigFlagContext(OSContextGenerator):
4588-
4589- """
4590- Responsible for adding user-defined config-flags in charm config to a
4591- template context.
4592-
4593- NOTE: the value of config-flags may be a comma-separated list of
4594- key=value pairs and some Openstack config files support
4595- comma-separated lists as values.
4596- """
4597-
4598- def __call__(self):
4599- config_flags = config('config-flags')
4600- if not config_flags:
4601- return {}
4602-
4603- flags = config_flags_parser(config_flags)
4604- return {'user_config_flags': flags}
4605+ """Provides support for user-defined config flags.
4606+
4607+ Users can define a comma-seperated list of key=value pairs
4608+ in the charm configuration and apply them at any point in
4609+ any file by using a template flag.
4610+
4611+ Sometimes users might want config flags inserted within a
4612+ specific section so this class allows users to specify the
4613+ template flag name, allowing for multiple template flags
4614+ (sections) within the same context.
4615+
4616+ NOTE: the value of config-flags may be a comma-separated list of
4617+ key=value pairs and some Openstack config files support
4618+ comma-separated lists as values.
4619+ """
4620+
4621+ def __init__(self, charm_flag='config-flags',
4622+ template_flag='user_config_flags'):
4623+ """
4624+ :param charm_flag: config flags in charm configuration.
4625+ :param template_flag: insert point for user-defined flags in template
4626+ file.
4627+ """
4628+ super(OSConfigFlagContext, self).__init__()
4629+ self._charm_flag = charm_flag
4630+ self._template_flag = template_flag
4631+
4632+ def __call__(self):
4633+ config_flags = config(self._charm_flag)
4634+ if not config_flags:
4635+ return {}
4636+
4637+ return {self._template_flag:
4638+ config_flags_parser(config_flags)}
4639
4640
4641 class SubordinateConfigContext(OSContextGenerator):
4642@@ -611,7 +1082,7 @@
4643 The subordinate interface allows subordinates to export their
4644 configuration requirements to the principle for multiple config
4645 files and multiple serivces. Ie, a subordinate that has interfaces
4646- to both glance and nova may export to following yaml blob as json:
4647+ to both glance and nova may export to following yaml blob as json::
4648
4649 glance:
4650 /etc/glance/glance-api.conf:
4651@@ -630,7 +1101,8 @@
4652
4653 It is then up to the principle charms to subscribe this context to
4654 the service+config file it is interestd in. Configuration data will
4655- be available in the template context, in glance's case, as:
4656+ be available in the template context, in glance's case, as::
4657+
4658 ctxt = {
4659 ... other context ...
4660 'subordinate_config': {
4661@@ -642,7 +1114,6 @@
4662 },
4663 }
4664 }
4665-
4666 """
4667
4668 def __init__(self, service, config_file, interface):
4669@@ -652,13 +1123,22 @@
4670 :param config_file : Service's config file to query sections
4671 :param interface : Subordinate interface to inspect
4672 """
4673- self.service = service
4674 self.config_file = config_file
4675- self.interface = interface
4676+ if isinstance(service, list):
4677+ self.services = service
4678+ else:
4679+ self.services = [service]
4680+ if isinstance(interface, list):
4681+ self.interfaces = interface
4682+ else:
4683+ self.interfaces = [interface]
4684
4685 def __call__(self):
4686- ctxt = {}
4687- for rid in relation_ids(self.interface):
4688+ ctxt = {'sections': {}}
4689+ rids = []
4690+ for interface in self.interfaces:
4691+ rids.extend(relation_ids(interface))
4692+ for rid in rids:
4693 for unit in related_units(rid):
4694 sub_config = relation_get('subordinate_configuration',
4695 rid=rid, unit=unit)
4696@@ -670,23 +1150,44 @@
4697 'setting from %s' % rid, level=ERROR)
4698 continue
4699
4700- if self.service not in sub_config:
4701- log('Found subordinate_config on %s but it contained'
4702- 'nothing for %s service' % (rid, self.service))
4703- continue
4704-
4705- sub_config = sub_config[self.service]
4706- if self.config_file not in sub_config:
4707- log('Found subordinate_config on %s but it contained'
4708- 'nothing for %s' % (rid, self.config_file))
4709- continue
4710-
4711- sub_config = sub_config[self.config_file]
4712- for k, v in sub_config.iteritems():
4713- ctxt[k] = v
4714-
4715- if not ctxt:
4716- ctxt['sections'] = {}
4717+ for service in self.services:
4718+ if service not in sub_config:
4719+ log('Found subordinate_config on %s but it contained'
4720+ 'nothing for %s service' % (rid, service),
4721+ level=INFO)
4722+ continue
4723+
4724+ sub_config = sub_config[service]
4725+ if self.config_file not in sub_config:
4726+ log('Found subordinate_config on %s but it contained'
4727+ 'nothing for %s' % (rid, self.config_file),
4728+ level=INFO)
4729+ continue
4730+
4731+ sub_config = sub_config[self.config_file]
4732+ for k, v in six.iteritems(sub_config):
4733+ if k == 'sections':
4734+ for section, config_list in six.iteritems(v):
4735+ log("adding section '%s'" % (section),
4736+ level=DEBUG)
4737+ if ctxt[k].get(section):
4738+ ctxt[k][section].extend(config_list)
4739+ else:
4740+ ctxt[k][section] = config_list
4741+ else:
4742+ ctxt[k] = v
4743+ log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
4744+ return ctxt
4745+
4746+
4747+class LogLevelContext(OSContextGenerator):
4748+
4749+ def __call__(self):
4750+ ctxt = {}
4751+ ctxt['debug'] = \
4752+ False if config('debug') is None else config('debug')
4753+ ctxt['verbose'] = \
4754+ False if config('verbose') is None else config('verbose')
4755
4756 return ctxt
4757
4758@@ -694,7 +1195,233 @@
4759 class SyslogContext(OSContextGenerator):
4760
4761 def __call__(self):
4762- ctxt = {
4763- 'use_syslog': config('use-syslog')
4764+ ctxt = {'use_syslog': config('use-syslog')}
4765+ return ctxt
4766+
4767+
4768+class BindHostContext(OSContextGenerator):
4769+
4770+ def __call__(self):
4771+ if config('prefer-ipv6'):
4772+ return {'bind_host': '::'}
4773+ else:
4774+ return {'bind_host': '0.0.0.0'}
4775+
4776+
4777+class WorkerConfigContext(OSContextGenerator):
4778+
4779+ @property
4780+ def num_cpus(self):
4781+ try:
4782+ from psutil import NUM_CPUS
4783+ except ImportError:
4784+ apt_install('python-psutil', fatal=True)
4785+ from psutil import NUM_CPUS
4786+
4787+ return NUM_CPUS
4788+
4789+ def __call__(self):
4790+ multiplier = config('worker-multiplier') or 0
4791+ ctxt = {"workers": self.num_cpus * multiplier}
4792+ return ctxt
4793+
4794+
4795+class ZeroMQContext(OSContextGenerator):
4796+ interfaces = ['zeromq-configuration']
4797+
4798+ def __call__(self):
4799+ ctxt = {}
4800+ if is_relation_made('zeromq-configuration', 'host'):
4801+ for rid in relation_ids('zeromq-configuration'):
4802+ for unit in related_units(rid):
4803+ ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
4804+ ctxt['zmq_host'] = relation_get('host', unit, rid)
4805+ ctxt['zmq_redis_address'] = relation_get(
4806+ 'zmq_redis_address', unit, rid)
4807+
4808+ return ctxt
4809+
4810+
4811+class NotificationDriverContext(OSContextGenerator):
4812+
4813+ def __init__(self, zmq_relation='zeromq-configuration',
4814+ amqp_relation='amqp'):
4815+ """
4816+ :param zmq_relation: Name of Zeromq relation to check
4817+ """
4818+ self.zmq_relation = zmq_relation
4819+ self.amqp_relation = amqp_relation
4820+
4821+ def __call__(self):
4822+ ctxt = {'notifications': 'False'}
4823+ if is_relation_made(self.amqp_relation):
4824+ ctxt['notifications'] = "True"
4825+
4826+ return ctxt
4827+
4828+
4829+class SysctlContext(OSContextGenerator):
4830+ """This context check if the 'sysctl' option exists on configuration
4831+ then creates a file with the loaded contents"""
4832+ def __call__(self):
4833+ sysctl_dict = config('sysctl')
4834+ if sysctl_dict:
4835+ sysctl_create(sysctl_dict,
4836+ '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
4837+ return {'sysctl': sysctl_dict}
4838+
4839+
4840+class NeutronAPIContext(OSContextGenerator):
4841+ '''
4842+ Inspects current neutron-plugin-api relation for neutron settings. Return
4843+ defaults if it is not present.
4844+ '''
4845+ interfaces = ['neutron-plugin-api']
4846+
4847+ def __call__(self):
4848+ self.neutron_defaults = {
4849+ 'l2_population': {
4850+ 'rel_key': 'l2-population',
4851+ 'default': False,
4852+ },
4853+ 'overlay_network_type': {
4854+ 'rel_key': 'overlay-network-type',
4855+ 'default': 'gre',
4856+ },
4857+ 'neutron_security_groups': {
4858+ 'rel_key': 'neutron-security-groups',
4859+ 'default': False,
4860+ },
4861+ 'network_device_mtu': {
4862+ 'rel_key': 'network-device-mtu',
4863+ 'default': None,
4864+ },
4865+ 'enable_dvr': {
4866+ 'rel_key': 'enable-dvr',
4867+ 'default': False,
4868+ },
4869+ 'enable_l3ha': {
4870+ 'rel_key': 'enable-l3ha',
4871+ 'default': False,
4872+ },
4873 }
4874- return ctxt
4875+ ctxt = self.get_neutron_options({})
4876+ for rid in relation_ids('neutron-plugin-api'):
4877+ for unit in related_units(rid):
4878+ rdata = relation_get(rid=rid, unit=unit)
4879+ if 'l2-population' in rdata:
4880+ ctxt.update(self.get_neutron_options(rdata))
4881+
4882+ return ctxt
4883+
4884+ def get_neutron_options(self, rdata):
4885+ settings = {}
4886+ for nkey in self.neutron_defaults.keys():
4887+ defv = self.neutron_defaults[nkey]['default']
4888+ rkey = self.neutron_defaults[nkey]['rel_key']
4889+ if rkey in rdata.keys():
4890+ if type(defv) is bool:
4891+ settings[nkey] = bool_from_string(rdata[rkey])
4892+ else:
4893+ settings[nkey] = rdata[rkey]
4894+ else:
4895+ settings[nkey] = defv
4896+ return settings
4897+
4898+
4899+class ExternalPortContext(NeutronPortContext):
4900+
4901+ def __call__(self):
4902+ ctxt = {}
4903+ ports = config('ext-port')
4904+ if ports:
4905+ ports = [p.strip() for p in ports.split()]
4906+ ports = self.resolve_ports(ports)
4907+ if ports:
4908+ ctxt = {"ext_port": ports[0]}
4909+ napi_settings = NeutronAPIContext()()
4910+ mtu = napi_settings.get('network_device_mtu')
4911+ if mtu:
4912+ ctxt['ext_port_mtu'] = mtu
4913+
4914+ return ctxt
4915+
4916+
4917+class DataPortContext(NeutronPortContext):
4918+
4919+ def __call__(self):
4920+ ports = config('data-port')
4921+ if ports:
4922+ # Map of {port/mac:bridge}
4923+ portmap = parse_data_port_mappings(ports)
4924+ ports = portmap.keys()
4925+ # Resolve provided ports or mac addresses and filter out those
4926+ # already attached to a bridge.
4927+ resolved = self.resolve_ports(ports)
4928+ # FIXME: is this necessary?
4929+ normalized = {get_nic_hwaddr(port): port for port in resolved
4930+ if port not in ports}
4931+ normalized.update({port: port for port in resolved
4932+ if port in ports})
4933+ if resolved:
4934+ return {normalized[port]: bridge for port, bridge in
4935+ six.iteritems(portmap) if port in normalized.keys()}
4936+
4937+ return None
4938+
4939+
4940+class PhyNICMTUContext(DataPortContext):
4941+
4942+ def __call__(self):
4943+ ctxt = {}
4944+ mappings = super(PhyNICMTUContext, self).__call__()
4945+ if mappings and mappings.keys():
4946+ ports = sorted(mappings.keys())
4947+ napi_settings = NeutronAPIContext()()
4948+ mtu = napi_settings.get('network_device_mtu')
4949+ all_ports = set()
4950+ # If any of ports is a vlan device, its underlying device must have
4951+ # mtu applied first.
4952+ for port in ports:
4953+ for lport in glob.glob("/sys/class/net/%s/lower_*" % port):
4954+ lport = os.path.basename(lport)
4955+ all_ports.add(lport.split('_')[1])
4956+
4957+ all_ports = list(all_ports)
4958+ all_ports.extend(ports)
4959+ if mtu:
4960+ ctxt["devs"] = '\\n'.join(all_ports)
4961+ ctxt['mtu'] = mtu
4962+
4963+ return ctxt
4964+
4965+
4966+class NetworkServiceContext(OSContextGenerator):
4967+
4968+ def __init__(self, rel_name='quantum-network-service'):
4969+ self.rel_name = rel_name
4970+ self.interfaces = [rel_name]
4971+
4972+ def __call__(self):
4973+ for rid in relation_ids(self.rel_name):
4974+ for unit in related_units(rid):
4975+ rdata = relation_get(rid=rid, unit=unit)
4976+ ctxt = {
4977+ 'keystone_host': rdata.get('keystone_host'),
4978+ 'service_port': rdata.get('service_port'),
4979+ 'auth_port': rdata.get('auth_port'),
4980+ 'service_tenant': rdata.get('service_tenant'),
4981+ 'service_username': rdata.get('service_username'),
4982+ 'service_password': rdata.get('service_password'),
4983+ 'quantum_host': rdata.get('quantum_host'),
4984+ 'quantum_port': rdata.get('quantum_port'),
4985+ 'quantum_url': rdata.get('quantum_url'),
4986+ 'region': rdata.get('region'),
4987+ 'service_protocol':
4988+ rdata.get('service_protocol') or 'http',
4989+ 'auth_protocol':
4990+ rdata.get('auth_protocol') or 'http',
4991+ }
4992+ if self.context_complete(ctxt):
4993+ return ctxt
4994+ return {}
4995
4996=== added directory 'hooks/charmhelpers/contrib/openstack/files'
4997=== added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
4998--- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
4999+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-11-12 11:46:11 +0000
5000@@ -0,0 +1,18 @@
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: