Merge lp:~bcsaller/pyjuju/subordinate-charms into lp:pyjuju
- subordinate-charms
- Merge into trunk
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~bcsaller/pyjuju/subordinate-charms | ||||
Merge into: | lp:pyjuju | ||||
Diff against target: |
749 lines (+528/-22) 12 files modified
.bzrignore (+2/-2) docs/source/charm-upgrades.rst (+1/-2) docs/source/charm.rst (+14/-0) docs/source/drafts/implicit-relations.rst (+62/-0) docs/source/drafts/subordinate-internals.rst (+168/-0) docs/source/drafts/subordinate-services.rst (+170/-0) juju/charm/metadata.py (+44/-4) juju/charm/tests/repository/series/logging/.ignored (+1/-0) juju/charm/tests/repository/series/logging/hooks/install (+2/-0) juju/charm/tests/repository/series/logging/metadata.yaml (+13/-0) juju/charm/tests/repository/series/logging/revision (+1/-0) juju/charm/tests/test_metadata.py (+50/-14) |
||||
To merge this branch: | bzr merge lp:~bcsaller/pyjuju/subordinate-charms | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Needs Information | ||
Review via email: mp+84562@code.launchpad.net |
This proposal has been superseded by a proposal from 2012-02-09.
Commit message
Description of the change
charm metadata support for subordinates
Charm directory, bundle and metadata support for subordinate charms and scoped relations
Benjamin Saller (bcsaller) wrote : | # |
Reviewers: mp+84562_
Message:
Please take a look.
Description:
Charm directory, bundle and metadata support for subordinate interfaces
Includes changes related to the first round of feedback
https:/
(do not edit description out of merge proposal)
Please review this at https:/
Affected files:
M .bzrignore
M docs/source/
A docs/source/
A docs/source/
A docs/source/
M juju/charm/
A juju/charm/
A juju/charm/
A juju/charm/
A juju/charm/
M juju/charm/
Benjamin Saller (bcsaller) wrote : | # |
This was pushed in error, still pending the spec review so I can get the naming to what we agree on.
Gustavo Niemeyer (niemeyer) wrote : | # |
On 2012/01/19 08:59:45, bcsaller wrote:
> Please take a look.
Please hold off on merging this while we talk in the mailing list about
the specification.
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
- 470. By Benjamin Saller
-
Merged subordinate-spec into subordinate-charms.
- 471. By Benjamin Saller
-
use defines throughout, is_subordinate only checks metadata flag
- 472. By Benjamin Saller
-
Merged subordinate-spec into subordinate-charms.
Kapil Thangavelu (hazmat) wrote : | # |
lgtm +1, one minor. woot! first branch of subordinates.
https:/
File juju/charm/
https:/
juju/charm/
Feels like this should be an error that's raised.
- 473. By Benjamin Saller
-
Merged subordinate-spec into subordinate-charms.
- 474. By Benjamin Saller
-
Merged subordinate-spec into subordinate-charms.
- 475. By Benjamin Saller
-
Merged subordinate-spec into subordinate-charms.
- 476. By Benjamin Saller
-
raise error when subordinate is improperly used
Benjamin Saller (bcsaller) wrote : | # |
https:/
File juju/charm/
https:/
juju/charm/
On 2012/02/27 19:21:20, hazmat wrote:
> Feels like this should be an error that's raised.
It isn't currently because the other checks in the method only log.
However you're correct that this indicates a usage error and should be
corrected so I'm changing it to a MetadataError(...)
Unmerged revisions
Preview Diff
1 | === modified file '.bzrignore' | |||
2 | --- .bzrignore 2011-09-05 10:53:18 +0000 | |||
3 | +++ .bzrignore 2012-02-09 03:51:19 +0000 | |||
4 | @@ -1,6 +1,6 @@ | |||
5 | 1 | /debian/files | ||
6 | 2 | /docs/build | 1 | /docs/build |
7 | 3 | /docs/source/generated | 2 | /docs/source/generated |
8 | 4 | /_trial_temp | 3 | /_trial_temp |
9 | 5 | /tags | 4 | /tags |
11 | 6 | zookeeper.log | 5 | /zookeeper.log |
12 | 6 | /TAGS | ||
13 | 7 | 7 | ||
14 | === modified file 'docs/source/charm-upgrades.rst' | |||
15 | --- docs/source/charm-upgrades.rst 2011-09-15 17:56:23 +0000 | |||
16 | +++ docs/source/charm-upgrades.rst 2012-02-09 03:51:19 +0000 | |||
17 | @@ -5,7 +5,7 @@ | |||
18 | 5 | Upgrading a charm | 5 | Upgrading a charm |
19 | 6 | ------------------- | 6 | ------------------- |
20 | 7 | 7 | ||
22 | 8 | A charm_ can be upgraded via the command line using the following | 8 | :doc:`charm` can be upgraded via the command line using the following |
23 | 9 | syntax:: | 9 | syntax:: |
24 | 10 | 10 | ||
25 | 11 | $ juju upgrade-charm <service-name> | 11 | $ juju upgrade-charm <service-name> |
26 | @@ -21,7 +21,6 @@ | |||
27 | 21 | The unit agent will switch over to executing hooks from the new charm, | 21 | The unit agent will switch over to executing hooks from the new charm, |
28 | 22 | after executing the `upgrade-charm` hook. | 22 | after executing the `upgrade-charm` hook. |
29 | 23 | 23 | ||
30 | 24 | .. _charm: ../charm.html | ||
31 | 25 | 24 | ||
32 | 26 | 25 | ||
33 | 27 | Charm upgrade support | 26 | Charm upgrade support |
34 | 28 | 27 | ||
35 | === modified file 'docs/source/charm.rst' | |||
36 | --- docs/source/charm.rst 2011-10-06 21:04:13 +0000 | |||
37 | +++ docs/source/charm.rst 2012-02-09 03:51:19 +0000 | |||
38 | @@ -74,6 +74,20 @@ | |||
39 | 74 | means the relation is required. While you may define it, this | 74 | means the relation is required. While you may define it, this |
40 | 75 | field is not yet enforced by juju. | 75 | field is not yet enforced by juju. |
41 | 76 | 76 | ||
42 | 77 | * **scope:** - Controls which units of related-to services can | ||
43 | 78 | be communicated with through this relationship. Juju supports | ||
44 | 79 | the following scopes: | ||
45 | 80 | |||
46 | 81 | * **global:** `default` When a traditional relation is added | ||
47 | 82 | between two services, all the service units for the first service will | ||
48 | 83 | receive relation events about all service units for the second | ||
49 | 84 | service. | ||
50 | 85 | |||
51 | 86 | * **container**: Communication is restricted to units deployed | ||
52 | 87 | in the same container. It is not possible to establish a | ||
53 | 88 | container scoped relation between principal services. See | ||
54 | 89 | :doc:`subordinate-services`. | ||
55 | 90 | |||
56 | 77 | As a shortcut, if these properties are not defined, and instead | 91 | As a shortcut, if these properties are not defined, and instead |
57 | 78 | a single string value is provided next to the relation name, the | 92 | a single string value is provided next to the relation name, the |
58 | 79 | string is taken as the interface value, as seen in this | 93 | string is taken as the interface value, as seen in this |
59 | 80 | 94 | ||
60 | === added file 'docs/source/drafts/implicit-relations.rst' | |||
61 | --- docs/source/drafts/implicit-relations.rst 1970-01-01 00:00:00 +0000 | |||
62 | +++ docs/source/drafts/implicit-relations.rst 2012-02-09 03:51:19 +0000 | |||
63 | @@ -0,0 +1,62 @@ | |||
64 | 1 | Implicit relations | ||
65 | 2 | =================== | ||
66 | 3 | |||
67 | 4 | Implicit relations allow for interested services to gather | ||
68 | 5 | lifecycle-oriented events and data about other services without | ||
69 | 6 | expecting or requiring any modifications on the part of the author of | ||
70 | 7 | the other service's charm. | ||
71 | 8 | |||
72 | 9 | Implicit relationships are named in the reserved the juju-* | ||
73 | 10 | namespace. Both the relation name and interface names provided by juju | ||
74 | 11 | are prefixed with `juju-`. Charms attempting to provide new | ||
75 | 12 | relationships in this namespace will trigger an error. | ||
76 | 13 | |||
77 | 14 | Juju currently provides one implicit relationship to all deployed | ||
78 | 15 | services. | ||
79 | 16 | |||
80 | 17 | `juju-info`, if specified would look like:: | ||
81 | 18 | |||
82 | 19 | provides: | ||
83 | 20 | juju-info: | ||
84 | 21 | interface: juju-info | ||
85 | 22 | |||
86 | 23 | The charm author should not declare the `juju-info` relation and is | ||
87 | 24 | provided here only as an example. The `juju-info` relation is | ||
88 | 25 | implicitly provided by all charms, and enables the requiring unit to | ||
89 | 26 | obtain basic details about the related-to unit. The following settings | ||
90 | 27 | will be implicitly provided by the remote unit in a relation through its | ||
91 | 28 | `juju-info` relation :: | ||
92 | 29 | |||
93 | 30 | private-address | ||
94 | 31 | public-address | ||
95 | 32 | |||
96 | 33 | |||
97 | 34 | Relationship resolution | ||
98 | 35 | ----------------------- | ||
99 | 36 | |||
100 | 37 | If **rsyslog** is a :doc:`subordinate charm<subordinate-services>` and | ||
101 | 38 | requires a valid `scope: container` relationship in order to | ||
102 | 39 | deploy. It can take advantage of optional support from the principal | ||
103 | 40 | charm but in the event that the principal charm doesn't provide this | ||
104 | 41 | support it will still require a `scope: container` relationship. In | ||
105 | 42 | this event the logging charm author can take advantage of the implicit | ||
106 | 43 | relationship offered by all charms, `juju-info`. :: | ||
107 | 44 | |||
108 | 45 | requires: | ||
109 | 46 | logging: | ||
110 | 47 | interface: logging-directory | ||
111 | 48 | scope: container | ||
112 | 49 | juju-info: | ||
113 | 50 | interface: juju-info | ||
114 | 51 | scope: container | ||
115 | 52 | |||
116 | 53 | The admin then issues the following :: | ||
117 | 54 | |||
118 | 55 | juju add-relation wordpress rsyslog | ||
119 | 56 | |||
120 | 57 | If the wordpress author doesn't provide the `logging-directory` | ||
121 | 58 | interface juju will use the less-specific (in the sense that it likely | ||
122 | 59 | provides less information) `juju-info` interface. | ||
123 | 60 | |||
124 | 61 | juju always attempts to match user provided interfaces before looking | ||
125 | 62 | for possible relationship matches in the `juju-*` namespace. | ||
126 | 0 | 63 | ||
127 | === added file 'docs/source/drafts/subordinate-internals.rst' | |||
128 | --- docs/source/drafts/subordinate-internals.rst 1970-01-01 00:00:00 +0000 | |||
129 | +++ docs/source/drafts/subordinate-internals.rst 2012-02-09 03:51:19 +0000 | |||
130 | @@ -0,0 +1,168 @@ | |||
131 | 1 | Subordinate service implementation details | ||
132 | 2 | =========================================== | ||
133 | 3 | |||
134 | 4 | |||
135 | 5 | This document explains the implementation of subordinate services. For | ||
136 | 6 | a higher level understanding please refer to the primary :doc:`subordinates | ||
137 | 7 | document <subordinate-services>`. | ||
138 | 8 | |||
139 | 9 | |||
140 | 10 | Overview | ||
141 | 11 | ------- | ||
142 | 12 | |||
143 | 13 | Principal services can have relationships with subordinates. This is | ||
144 | 14 | modeled using extensions to the `client-server` relationship | ||
145 | 15 | type. This new relation type is used exclusively between subordinates | ||
146 | 16 | and principals and is used to limit communication to only service | ||
147 | 17 | units in the same container. | ||
148 | 18 | |||
149 | 19 | |||
150 | 20 | ZooKeeper state | ||
151 | 21 | --------------- | ||
152 | 22 | |||
153 | 23 | Subordinate relations use the normal `client-server` relationship type | ||
154 | 24 | but store additional information in the relation nodes to maintain a | ||
155 | 25 | 1-1 mapping from the unit-id of the principal to the unit id of the | ||
156 | 26 | subordinate. :: | ||
157 | 27 | |||
158 | 28 | relations/ | ||
159 | 29 | relation-000000001/ | ||
160 | 30 | container_relations/ | ||
161 | 31 | | subordinate_id-00000002: principal_id-00000001 | ||
162 | 32 | | subordinate_id-00000004: principal_id-00000003 | ||
163 | 33 | --------------------------------------------------- | ||
164 | 34 | settings/ | ||
165 | 35 | principal_id-00000001/ | ||
166 | 36 | | private-address: 10.2.2.2 | ||
167 | 37 | ------------------------------- | ||
168 | 38 | principal_id-00000003/ | ||
169 | 39 | | private-address: 10.2.2.3 | ||
170 | 40 | --------------------------------------------------- | ||
171 | 41 | |||
172 | 42 | Elements ending with **/** refer to ZK nodes with its children | ||
173 | 43 | indented under it.. Elements preceded by **|** represent a YAML | ||
174 | 44 | encoded structure under the preceding ZK node. `container_relations` | ||
175 | 45 | is a new structure under the each relations ZK node. This contains a | ||
176 | 46 | mapping from the subordinate unit's id to the id of its principal | ||
177 | 47 | services' unit agent. This representation takes advantage of the fact | ||
178 | 48 | that while a principal can have many subordinates a subordinate can | ||
179 | 49 | only have one principal. In the context of a given relationship this | ||
180 | 50 | mapping is unique. | ||
181 | 51 | |||
182 | 52 | Additionally there are changes related to the storage of the relation | ||
183 | 53 | in the topology. A `relation_scope` is added to the ordered list of | ||
184 | 54 | properties such that :: | ||
185 | 55 | |||
186 | 56 | relations | ||
187 | 57 | relation-00000001: | ||
188 | 58 | - relation_type | ||
189 | 59 | - relation_scope | ||
190 | 60 | - service-00000001: {name: name, role: role} | ||
191 | 61 | service-00000002: {name: name, role: role} | ||
192 | 62 | |||
193 | 63 | |||
194 | 64 | Watch related changes | ||
195 | 65 | --------------------- | ||
196 | 66 | |||
197 | 67 | The unit agent will use watch_relation_states to see when new | ||
198 | 68 | relations are added. When a subordinate relation is present deployment | ||
199 | 69 | actions will be taken in the unit agents lifecycle. | ||
200 | 70 | |||
201 | 71 | UnitRelationStates.watch_related_units will dispatch on the relation | ||
202 | 72 | and only establish watches between the principal and subordinate units | ||
203 | 73 | in the same container. | ||
204 | 74 | |||
205 | 75 | Deployment | ||
206 | 76 | ---------- | ||
207 | 77 | |||
208 | 78 | `juju deploy` needs to enforce that services which require `scope: | ||
209 | 79 | container` relations are not deployed until one of those relationships | ||
210 | 80 | have been satisfied. | ||
211 | 81 | |||
212 | 82 | The UnitMachineDeployment/UnitContainerDeployment in machine/unit will | ||
213 | 83 | undergo minor refactoring to make it more easily useable by the | ||
214 | 84 | UnitAgent to do its deployment of subordinate services. The Unit Agent | ||
215 | 85 | is the only entity with direct a relationship to its subordinate unit | ||
216 | 86 | and so the UnitAgent does the deployment of its subordinate units | ||
217 | 87 | rather than the MachineAgent. This model will continue to work in | ||
218 | 88 | the expected LXCEverywhere future. | ||
219 | 89 | |||
220 | 90 | When a subordinate unit is deployed it is assigned the public and private | ||
221 | 91 | addresses of its principal service (even though it may expose its own | ||
222 | 92 | ports). This is because networking is dependent on the container its | ||
223 | 93 | running it, i.e, that of the principal's service unit. | ||
224 | 94 | |||
225 | 95 | One interesting caveat is that we don't assign the subordinate unit a | ||
226 | 96 | machine id. `juju-status` exposes this information in a way that is | ||
227 | 97 | clear and is outlined below. Machine assignment is currently used to | ||
228 | 98 | trigger a class of deployment activities which subordinate services do | ||
229 | 99 | not take advantage of. It is an error to assign a machine directly to | ||
230 | 100 | a subordinate unit (as this indicates a usage error). | ||
231 | 101 | |||
232 | 102 | `juju.unit.lifecycle._process_service_changes` is currently | ||
233 | 103 | responsible for adding unit state to relations. This code will change | ||
234 | 104 | so that when relationships are added or removed we have access to the | ||
235 | 105 | unit_name of the subordinate. This information is used to annotate the | ||
236 | 106 | UnitRelationState with the mapping from principal unit_name to | ||
237 | 107 | subordinate unit name and is stored in ZooKeeper as outlined | ||
238 | 108 | above. When a relationship is removed we detect this here as well and | ||
239 | 109 | update the mapping using the unit_name of the principal. | ||
240 | 110 | |||
241 | 111 | `juju.state.relation.ServiceRelationState.add_unit_state` will be | ||
242 | 112 | augmented to support tracking of the 1-1 mapping between principal and | ||
243 | 113 | subordinate unit names. It will take an optional `principal_unit` | ||
244 | 114 | argument. This will take the `unit_name` the of principal unit the | ||
245 | 115 | service is contained with. Error checking will validate that the | ||
246 | 116 | service unit should be subordinate to the principal service in | ||
247 | 117 | question. | ||
248 | 118 | |||
249 | 119 | |||
250 | 120 | Unit management | ||
251 | 121 | --------------- | ||
252 | 122 | |||
253 | 123 | `juju add-unit` must raise an error informing the admin they can not | ||
254 | 124 | add units of a subordinate service. These scale automatically with the | ||
255 | 125 | principal service. | ||
256 | 126 | |||
257 | 127 | `juju remove-unit` produces an error on subordinate services for the | ||
258 | 128 | same reason. | ||
259 | 129 | |||
260 | 130 | |||
261 | 131 | Relation management | ||
262 | 132 | ------------------- | ||
263 | 133 | |||
264 | 134 | `juju add-relation` and `juju remove-relation` must trigger the | ||
265 | 135 | deployment and removal of subordinate units. This is done using the | ||
266 | 136 | watch machinery outlined above. Each principal service unit will | ||
267 | 137 | deploy a new unit agent for its subordinate when the appropriate new | ||
268 | 138 | relationship is added and remove it when the relationship is departed. | ||
269 | 139 | |||
270 | 140 | The subordinate service will maintain a watch of its relationship to | ||
271 | 141 | the principal and should this relationship be removed the subordinate | ||
272 | 142 | will transition its state to stopped and then remove its own state | ||
273 | 143 | from the container and terminate. This will require follow up work to | ||
274 | 144 | handle the proper triggering of stop hooks on the subordinate units | ||
275 | 145 | which isn't handled | ||
276 | 146 | |||
277 | 147 | Status | ||
278 | 148 | ------ | ||
279 | 149 | |||
280 | 150 | The changes to status are outlined in the user oriented documentation. | ||
281 | 151 | |||
282 | 152 | |||
283 | 153 | Roadmap | ||
284 | 154 | ------- | ||
285 | 155 | |||
286 | 156 | This serves as a guide to the planned merge tree. :: | ||
287 | 157 | |||
288 | 158 | subordinate-spec | ||
289 | 159 | subordinate-charms # (charm metadata support) | ||
290 | 160 | subordinate-implicit-interfaces # (juju-info) | ||
291 | 161 | subordinate-relation-type | ||
292 | 162 | |||
293 | 163 | subordinate-control-deploy | ||
294 | 164 | subordinate-control-units # (add/remove) | ||
295 | 165 | subordinate-control-status | ||
296 | 166 | |||
297 | 167 | subordinate-control-relations # (add/remove) | ||
298 | 168 | subordinate-unit-agent-deploy | ||
299 | 0 | 169 | ||
300 | === added file 'docs/source/drafts/subordinate-services.rst' | |||
301 | --- docs/source/drafts/subordinate-services.rst 1970-01-01 00:00:00 +0000 | |||
302 | +++ docs/source/drafts/subordinate-services.rst 2012-02-09 03:51:19 +0000 | |||
303 | @@ -0,0 +1,170 @@ | |||
304 | 1 | Subordinate services | ||
305 | 2 | ===================== | ||
306 | 3 | |||
307 | 4 | Services are composed of one or more service units. A service unit | ||
308 | 5 | runs the service's software and is the smallest entity managed by | ||
309 | 6 | juju. Service units are typically run in an isolated container on a | ||
310 | 7 | machine with no knowledge or access to other services deployed onto | ||
311 | 8 | the same machine. Subordinate services allows for units of different | ||
312 | 9 | services to be deployed into the same container and to have knowledge | ||
313 | 10 | of each other. | ||
314 | 11 | |||
315 | 12 | |||
316 | 13 | Motivations | ||
317 | 14 | ----------- | ||
318 | 15 | |||
319 | 16 | Services such as logging, monitoring, backups and some types of | ||
320 | 17 | storage often require some access to the runtime of the service they | ||
321 | 18 | wish to operate on. Under the current modeling of services it is only | ||
322 | 19 | possible to relate services to other services with an explicit | ||
323 | 20 | interface pairing. Requiring a specified relation implies that every | ||
324 | 21 | charm author need be aware of any and all services a deployment might | ||
325 | 22 | wish to depend on, even if the other service can operate without any | ||
326 | 23 | explicit cooperation. For example a logging service may only require | ||
327 | 24 | access to the container level logging directory to function. | ||
328 | 25 | |||
329 | 26 | The following changes are designed to address these issues and allow a | ||
330 | 27 | class of charm that can execute in the context of an existing | ||
331 | 28 | container while still taking advantage of the existing relationship | ||
332 | 29 | machinery. | ||
333 | 30 | |||
334 | 31 | |||
335 | 32 | Terms | ||
336 | 33 | ----- | ||
337 | 34 | |||
338 | 35 | Principal service | ||
339 | 36 | A traditional service or charm in whose container subordinate | ||
340 | 37 | services will execute. | ||
341 | 38 | |||
342 | 39 | Subordinate service/charm | ||
343 | 40 | A service designed for and deployed to the running container of | ||
344 | 41 | another service unit. | ||
345 | 42 | |||
346 | 43 | Subordinate relation | ||
347 | 44 | A qualified relation type between principal services and their | ||
348 | 45 | subordinate service units. While modeled identically to | ||
349 | 46 | traditional relationships, juju only implements the relationship | ||
350 | 47 | between the unit of the principal and the subordinate service or | ||
351 | 48 | charm in the same container. | ||
352 | 49 | |||
353 | 50 | |||
354 | 51 | Relations | ||
355 | 52 | --------- | ||
356 | 53 | |||
357 | 54 | When a traditional relation is added between two services, all the | ||
358 | 55 | service units for the first service will receive relation events about | ||
359 | 56 | all service units for the second service. Subordinate services have a | ||
360 | 57 | very tight relationship with their principal service, so it makes | ||
361 | 58 | sense to be able to restrict that communication in some cases so that | ||
362 | 59 | they only receive events about each other. That's precisely what | ||
363 | 60 | happens when a relation is tagged as being a scoped to the | ||
364 | 61 | container. See :doc:`scoped relations<charm>`. | ||
365 | 62 | |||
366 | 63 | Container relations exist because they simplify responsibilities for | ||
367 | 64 | the subordinate service charm author who would otherwise always have | ||
368 | 65 | to filter units of their relation before finding the unit they can | ||
369 | 66 | operate on. | ||
370 | 67 | |||
371 | 68 | If a subordinate service needs to communicate with all units of the | ||
372 | 69 | principal service, it can still establish a traditional | ||
373 | 70 | (non-container) relationship to it. | ||
374 | 71 | |||
375 | 72 | In order to deploy a subordinate service a `scope: container` | ||
376 | 73 | relationship is required. Even when the principal services' charm | ||
377 | 74 | author doesn't provide an explicit relationship for the subordinate to | ||
378 | 75 | join, using an :doc:`implicit relation<implicit-relations>` with | ||
379 | 76 | `scope: container` will satisfy this constraint. | ||
380 | 77 | |||
381 | 78 | |||
382 | 79 | Addressability | ||
383 | 80 | -------------- | ||
384 | 81 | |||
385 | 82 | No special changes are made for the purpose of naming or addressing | ||
386 | 83 | subordinate units. If a subordinate logging service is deployed with a | ||
387 | 84 | single unit of wordpress we would expect the logging unit to be | ||
388 | 85 | addressable as logging/0, if this service were then related to a mysql | ||
389 | 86 | service with a single unit we'd expect logging/1 to be deployed in its | ||
390 | 87 | container. Subordinate units inherit the public/private address of the | ||
391 | 88 | principal service. The container of the principal defines the network | ||
392 | 89 | setup. | ||
393 | 90 | |||
394 | 91 | |||
395 | 92 | Declaring subordinate charms | ||
396 | 93 | ---------------------------- | ||
397 | 94 | |||
398 | 95 | When a charm author wishes to indicate their charm should operate as a | ||
399 | 96 | subordinate service only a small changes to the subordinate charms | ||
400 | 97 | metadata is required. Declaring a required interface with `scope: | ||
401 | 98 | container` in the interface definition of the charms metadata will | ||
402 | 99 | result in a subordinate deployment. Subordinate services may still | ||
403 | 100 | declare traditional relations to any service. The deployment is | ||
404 | 101 | delayed until a container relation is added. | ||
405 | 102 | |||
406 | 103 | The example below shows adding a container relation to a charm. :: | ||
407 | 104 | |||
408 | 105 | requires: | ||
409 | 106 | logging-directory: | ||
410 | 107 | interface: logging | ||
411 | 108 | scope: container | ||
412 | 109 | |||
413 | 110 | |||
414 | 111 | |||
415 | 112 | Status of subordinates | ||
416 | 113 | ---------------------- | ||
417 | 114 | |||
418 | 115 | The status output contains details about subordinate units under the | ||
419 | 116 | status of the principal service unit that it is sharing the container | ||
420 | 117 | with. The subordinate unit's output matches the formatting of existing | ||
421 | 118 | unit entries but omits `machine`, `public-address` and `subordinates` | ||
422 | 119 | (which are all the same as the principal unit). | ||
423 | 120 | |||
424 | 121 | The subordinate service is listed in the top level `services` | ||
425 | 122 | dictionary in an abbreviated form. The `subordinate-to: []` list is | ||
426 | 123 | added to the service which contains the names of all services this | ||
427 | 124 | service is subordinate to. `units` is displayed as a list of principal | ||
428 | 125 | unit names under which instances of this service are found. :: | ||
429 | 126 | |||
430 | 127 | services: | ||
431 | 128 | logging: | ||
432 | 129 | charm: local:series/logging-1 | ||
433 | 130 | subordinate-to: [wordpress] | ||
434 | 131 | relations: | ||
435 | 132 | logging-directory: wordpress | ||
436 | 133 | wordpress: | ||
437 | 134 | machine: 0 | ||
438 | 135 | public-address: wordpress-0.example.com | ||
439 | 136 | charm: local:series/wordpress-3 | ||
440 | 137 | relations: {loggin: logging} | ||
441 | 138 | units: | ||
442 | 139 | wordpress/0: | ||
443 | 140 | relations: | ||
444 | 141 | logging: {state: up} | ||
445 | 142 | state: started | ||
446 | 143 | subordinates: | ||
447 | 144 | logging/0: | ||
448 | 145 | relations: | ||
449 | 146 | logging: {state: up} | ||
450 | 147 | |||
451 | 148 | |||
452 | 149 | |||
453 | 150 | Usage | ||
454 | 151 | ----- | ||
455 | 152 | |||
456 | 153 | Assume the following deployment:: | ||
457 | 154 | |||
458 | 155 | juju deploy mysql | ||
459 | 156 | juju deploy wordpress | ||
460 | 157 | juju add-relation mysql wordpress | ||
461 | 158 | |||
462 | 159 | Now we'll create a subordinate logging service:: | ||
463 | 160 | |||
464 | 161 | juju deploy logging | ||
465 | 162 | juju add-relation logging mysql | ||
466 | 163 | juju add-relation logging wordpress | ||
467 | 164 | |||
468 | 165 | This will create a logging service unit inside each of the containers | ||
469 | 166 | holding the mysql and wordpress units. The logging service has a | ||
470 | 167 | standard client-server relation to both wordpress and mysql but these | ||
471 | 168 | new relationships are implemented only between the principal unit and | ||
472 | 169 | the subordinate unit . A subordinate unit may still have standard | ||
473 | 170 | relations established with any unit in its environment as usual. | ||
474 | 0 | 171 | ||
475 | === modified file 'juju/charm/metadata.py' | |||
476 | --- juju/charm/metadata.py 2011-11-14 15:58:01 +0000 | |||
477 | +++ juju/charm/metadata.py 2012-02-09 03:51:19 +0000 | |||
478 | @@ -15,10 +15,16 @@ | |||
479 | 15 | 15 | ||
480 | 16 | UTF8_SCHEMA = UnicodeOrString("utf-8") | 16 | UTF8_SCHEMA = UnicodeOrString("utf-8") |
481 | 17 | 17 | ||
482 | 18 | SCOPE_GLOBAL = "global" | ||
483 | 19 | SCOPE_CONTAINER = "container" | ||
484 | 20 | |||
485 | 21 | |||
486 | 18 | INTERFACE_SCHEMA = KeyDict({ | 22 | INTERFACE_SCHEMA = KeyDict({ |
490 | 19 | "interface": UTF8_SCHEMA, | 23 | "interface": UTF8_SCHEMA, |
491 | 20 | "limit": OneOf(Constant(None), Int()), | 24 | "limit": OneOf(Constant(None), Int()), |
492 | 21 | "optional": Bool()}) | 25 | "scope": OneOf(Constant(SCOPE_GLOBAL), Constant(SCOPE_CONTAINER)), |
493 | 26 | "optional": Bool()}, | ||
494 | 27 | optional=["scope"]) | ||
495 | 22 | 28 | ||
496 | 23 | 29 | ||
497 | 24 | class InterfaceExpander(object): | 30 | class InterfaceExpander(object): |
498 | @@ -66,6 +72,7 @@ | |||
499 | 66 | return { | 72 | return { |
500 | 67 | "interface": UTF8_SCHEMA.coerce(value, path), | 73 | "interface": UTF8_SCHEMA.coerce(value, path), |
501 | 68 | "limit": self.limit, | 74 | "limit": self.limit, |
502 | 75 | "scope": "global", | ||
503 | 69 | "optional": False} | 76 | "optional": False} |
504 | 70 | else: | 77 | else: |
505 | 71 | # Optional values are context-sensitive and/or have | 78 | # Optional values are context-sensitive and/or have |
506 | @@ -76,6 +83,7 @@ | |||
507 | 76 | value["limit"] = self.limit | 83 | value["limit"] = self.limit |
508 | 77 | if "optional" not in value: | 84 | if "optional" not in value: |
509 | 78 | value["optional"] = False | 85 | value["optional"] = False |
510 | 86 | value["scope"] = value.get("scope", "global") | ||
511 | 79 | return INTERFACE_SCHEMA.coerce(value, path) | 87 | return INTERFACE_SCHEMA.coerce(value, path) |
512 | 80 | 88 | ||
513 | 81 | 89 | ||
514 | @@ -87,7 +95,8 @@ | |||
515 | 87 | "peers": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)), | 95 | "peers": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)), |
516 | 88 | "provides": Dict(UTF8_SCHEMA, InterfaceExpander(limit=None)), | 96 | "provides": Dict(UTF8_SCHEMA, InterfaceExpander(limit=None)), |
517 | 89 | "requires": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)), | 97 | "requires": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)), |
519 | 90 | }, optional=set(["provides", "requires", "peers", "revision"])) | 98 | "subordinate": Bool(), |
520 | 99 | }, optional=set(["provides", "requires", "peers", "revision", "subordinate"])) | ||
521 | 91 | 100 | ||
522 | 92 | 101 | ||
523 | 93 | class MetaData(object): | 102 | class MetaData(object): |
524 | @@ -144,6 +153,26 @@ | |||
525 | 144 | """The charm peers relations.""" | 153 | """The charm peers relations.""" |
526 | 145 | return self._data.get("peers") | 154 | return self._data.get("peers") |
527 | 146 | 155 | ||
528 | 156 | @property | ||
529 | 157 | def is_subordinate(self): | ||
530 | 158 | """Indicates the charm requires a contained relationship. | ||
531 | 159 | |||
532 | 160 | This property will effect the deployment options of its | ||
533 | 161 | charm. When a charm is_subordinate it can only be deployed | ||
534 | 162 | when its contained relationship is satisfied. See the | ||
535 | 163 | subordinates specification. | ||
536 | 164 | """ | ||
537 | 165 | if self._data.get("subordinate", False) is False: | ||
538 | 166 | return False | ||
539 | 167 | |||
540 | 168 | if not self.requires: | ||
541 | 169 | return False | ||
542 | 170 | |||
543 | 171 | for relation_data in self.requires.values(): | ||
544 | 172 | if relation_data.get("scope") == "container": | ||
545 | 173 | return True | ||
546 | 174 | return False | ||
547 | 175 | |||
548 | 147 | def get_serialization_data(self): | 176 | def get_serialization_data(self): |
549 | 148 | """Get internal dictionary representing the state of this instance. | 177 | """Get internal dictionary representing the state of this instance. |
550 | 149 | 178 | ||
551 | @@ -187,6 +216,17 @@ | |||
552 | 187 | "%s: revision field is obsolete. Move it to the 'revision' " | 216 | "%s: revision field is obsolete. Move it to the 'revision' " |
553 | 188 | "file." % path) | 217 | "file." % path) |
554 | 189 | 218 | ||
555 | 219 | if self._data.get("subordinate", False) is True: | ||
556 | 220 | proper_subordinate = False | ||
557 | 221 | if self.requires: | ||
558 | 222 | for relation_data in self.requires.values(): | ||
559 | 223 | if relation_data.get("scope") == "container": | ||
560 | 224 | proper_subordinate = True | ||
561 | 225 | if not proper_subordinate: | ||
562 | 226 | log.warning( | ||
563 | 227 | "%s labeled subordinate but lacking scope:container `requires` relation", | ||
564 | 228 | path) | ||
565 | 229 | |||
566 | 190 | def parse_serialization_data(self, serialization_data, path=None): | 230 | def parse_serialization_data(self, serialization_data, path=None): |
567 | 191 | """Parse the unprocessed serialization data and load in this instance. | 231 | """Parse the unprocessed serialization data and load in this instance. |
568 | 192 | 232 | ||
569 | 193 | 233 | ||
570 | === added directory 'juju/charm/tests/repository/series/logging' | |||
571 | === added file 'juju/charm/tests/repository/series/logging/.ignored' | |||
572 | --- juju/charm/tests/repository/series/logging/.ignored 1970-01-01 00:00:00 +0000 | |||
573 | +++ juju/charm/tests/repository/series/logging/.ignored 2012-02-09 03:51:19 +0000 | |||
574 | @@ -0,0 +1,1 @@ | |||
575 | 1 | # | ||
576 | 0 | \ No newline at end of file | 2 | \ No newline at end of file |
577 | 1 | 3 | ||
578 | === added directory 'juju/charm/tests/repository/series/logging/hooks' | |||
579 | === added file 'juju/charm/tests/repository/series/logging/hooks/install' | |||
580 | --- juju/charm/tests/repository/series/logging/hooks/install 1970-01-01 00:00:00 +0000 | |||
581 | +++ juju/charm/tests/repository/series/logging/hooks/install 2012-02-09 03:51:19 +0000 | |||
582 | @@ -0,0 +1,2 @@ | |||
583 | 1 | #!/bin/bash | ||
584 | 2 | echo "Done!" | ||
585 | 0 | 3 | ||
586 | === added file 'juju/charm/tests/repository/series/logging/metadata.yaml' | |||
587 | --- juju/charm/tests/repository/series/logging/metadata.yaml 1970-01-01 00:00:00 +0000 | |||
588 | +++ juju/charm/tests/repository/series/logging/metadata.yaml 2012-02-09 03:51:19 +0000 | |||
589 | @@ -0,0 +1,13 @@ | |||
590 | 1 | name: logging | ||
591 | 2 | summary: "Subordinate logging test charm" | ||
592 | 3 | description: | | ||
593 | 4 | This is a longer description which | ||
594 | 5 | potentially contains multiple lines. | ||
595 | 6 | subordinate: true | ||
596 | 7 | provides: | ||
597 | 8 | logging-client: | ||
598 | 9 | interface: logging | ||
599 | 10 | requires: | ||
600 | 11 | logging-directory: | ||
601 | 12 | interface: logging | ||
602 | 13 | scope: container | ||
603 | 0 | 14 | ||
604 | === added file 'juju/charm/tests/repository/series/logging/revision' | |||
605 | --- juju/charm/tests/repository/series/logging/revision 1970-01-01 00:00:00 +0000 | |||
606 | +++ juju/charm/tests/repository/series/logging/revision 2012-02-09 03:51:19 +0000 | |||
607 | @@ -0,0 +1,1 @@ | |||
608 | 1 | 1 | ||
609 | 0 | \ No newline at end of file | 2 | \ No newline at end of file |
610 | 1 | 3 | ||
611 | === modified file 'juju/charm/tests/test_metadata.py' | |||
612 | --- juju/charm/tests/test_metadata.py 2011-11-08 14:30:44 +0000 | |||
613 | +++ juju/charm/tests/test_metadata.py 2012-02-09 03:51:19 +0000 | |||
614 | @@ -59,6 +59,7 @@ | |||
615 | 59 | self.assertEquals(self.metadata.obsolete_revision, None) | 59 | self.assertEquals(self.metadata.obsolete_revision, None) |
616 | 60 | self.assertEquals(self.metadata.summary, None) | 60 | self.assertEquals(self.metadata.summary, None) |
617 | 61 | self.assertEquals(self.metadata.description, None) | 61 | self.assertEquals(self.metadata.description, None) |
618 | 62 | self.assertEquals(self.metadata.is_subordinate, False) | ||
619 | 62 | 63 | ||
620 | 63 | def test_parse_and_check_basic_info(self): | 64 | def test_parse_and_check_basic_info(self): |
621 | 64 | """ | 65 | """ |
622 | @@ -72,6 +73,41 @@ | |||
623 | 72 | self.assertEquals(self.metadata.description, | 73 | self.assertEquals(self.metadata.description, |
624 | 73 | u"This is a longer description which\n" | 74 | u"This is a longer description which\n" |
625 | 74 | u"potentially contains multiple lines.\n") | 75 | u"potentially contains multiple lines.\n") |
626 | 76 | self.assertEquals(self.metadata.is_subordinate, False) | ||
627 | 77 | |||
628 | 78 | def test_is_subordinate(self): | ||
629 | 79 | """Validate rules for detecting proper subordinate charms are working""" | ||
630 | 80 | logging_path = os.path.join( | ||
631 | 81 | test_repository_path, "series", "logging", "metadata.yaml") | ||
632 | 82 | logging_configuration = open(logging_path).read() | ||
633 | 83 | self.metadata.parse(logging_configuration) | ||
634 | 84 | self.assertTrue(self.metadata.is_subordinate) | ||
635 | 85 | |||
636 | 86 | def test_subordinate_without_container_relation(self): | ||
637 | 87 | """Validate rules for detecting proper subordinate charms are working | ||
638 | 88 | |||
639 | 89 | Case where no container relation is specified. | ||
640 | 90 | """ | ||
641 | 91 | with self.change_sample() as data: | ||
642 | 92 | data["subordinate"] = True | ||
643 | 93 | log = self.capture_logging("juju.charm") | ||
644 | 94 | |||
645 | 95 | self.metadata.parse(self.sample, "some/path") | ||
646 | 96 | self.assertIn("some/path labeled subordinate but lacking scope:container `requires` relation", | ||
647 | 97 | log.getvalue()) | ||
648 | 98 | |||
649 | 99 | def test_scope_constraint(self): | ||
650 | 100 | """Verify the scope constrain is parsed properly.""" | ||
651 | 101 | logging_path = os.path.join( | ||
652 | 102 | test_repository_path, "series", "logging", "metadata.yaml") | ||
653 | 103 | logging_configuration = open(logging_path).read() | ||
654 | 104 | self.metadata.parse(logging_configuration) | ||
655 | 105 | # Verify the scope settings | ||
656 | 106 | self.assertEqual(self.metadata.provides[u"logging-client"]["scope"], | ||
657 | 107 | "global") | ||
658 | 108 | self.assertEqual(self.metadata.requires[u"logging-directory"]["scope"], | ||
659 | 109 | "container") | ||
660 | 110 | self.assertTrue(self.metadata.is_subordinate) | ||
661 | 75 | 111 | ||
662 | 76 | def assert_parse_with_revision(self, with_path): | 112 | def assert_parse_with_revision(self, with_path): |
663 | 77 | """ | 113 | """ |
664 | @@ -204,7 +240,7 @@ | |||
665 | 204 | self.assertEqual(metadata.peers, None) | 240 | self.assertEqual(metadata.peers, None) |
666 | 205 | self.assertEqual( | 241 | self.assertEqual( |
667 | 206 | metadata.provides["server"], | 242 | metadata.provides["server"], |
669 | 207 | {"interface": "mysql", "limit": None, "optional": False}) | 243 | {"interface": "mysql", "limit": None, "optional": False, "scope": "global"}) |
670 | 208 | self.assertEqual(metadata.requires, None) | 244 | self.assertEqual(metadata.requires, None) |
671 | 209 | 245 | ||
672 | 210 | def test_riak_sample(self): | 246 | def test_riak_sample(self): |
673 | @@ -212,13 +248,13 @@ | |||
674 | 212 | metadata = self.get_metadata("riak") | 248 | metadata = self.get_metadata("riak") |
675 | 213 | self.assertEqual( | 249 | self.assertEqual( |
676 | 214 | metadata.peers["ring"], | 250 | metadata.peers["ring"], |
678 | 215 | {"interface": "riak", "limit": 1, "optional": False}) | 251 | {"interface": "riak", "limit": 1, "optional": False, "scope": "global"}) |
679 | 216 | self.assertEqual( | 252 | self.assertEqual( |
680 | 217 | metadata.provides["endpoint"], | 253 | metadata.provides["endpoint"], |
682 | 218 | {"interface": "http", "limit": None, "optional": False}) | 254 | {"interface": "http", "limit": None, "optional": False, "scope": "global"}) |
683 | 219 | self.assertEqual( | 255 | self.assertEqual( |
684 | 220 | metadata.provides["admin"], | 256 | metadata.provides["admin"], |
686 | 221 | {"interface": "http", "limit": None, "optional": False}) | 257 | {"interface": "http", "limit": None, "optional": False, "scope": "global"}) |
687 | 222 | self.assertEqual(metadata.requires, None) | 258 | self.assertEqual(metadata.requires, None) |
688 | 223 | 259 | ||
689 | 224 | def test_wordpress_sample(self): | 260 | def test_wordpress_sample(self): |
690 | @@ -227,13 +263,13 @@ | |||
691 | 227 | self.assertEqual(metadata.peers, None) | 263 | self.assertEqual(metadata.peers, None) |
692 | 228 | self.assertEqual( | 264 | self.assertEqual( |
693 | 229 | metadata.provides["url"], | 265 | metadata.provides["url"], |
695 | 230 | {"interface": "http", "limit": None, "optional": False}) | 266 | {"interface": "http", "limit": None, "optional": False, "scope": "global"}) |
696 | 231 | self.assertEqual( | 267 | self.assertEqual( |
697 | 232 | metadata.requires["db"], | 268 | metadata.requires["db"], |
699 | 233 | {"interface": "mysql", "limit": 1, "optional": False}) | 269 | {"interface": "mysql", "limit": 1, "optional": False, "scope": "global"}) |
700 | 234 | self.assertEqual( | 270 | self.assertEqual( |
701 | 235 | metadata.requires["cache"], | 271 | metadata.requires["cache"], |
703 | 236 | {"interface": "varnish", "limit": 2, "optional": True}) | 272 | {"interface": "varnish", "limit": 2, "optional": True, "scope": "global"}) |
704 | 237 | 273 | ||
705 | 238 | def test_interface_expander(self): | 274 | def test_interface_expander(self): |
706 | 239 | """Test rewriting of a given interface specification into long form. | 275 | """Test rewriting of a given interface specification into long form. |
707 | @@ -252,22 +288,22 @@ | |||
708 | 252 | # shorthand is properly rewritten | 288 | # shorthand is properly rewritten |
709 | 253 | self.assertEqual( | 289 | self.assertEqual( |
710 | 254 | expander.coerce("http", ["provides"]), | 290 | expander.coerce("http", ["provides"]), |
712 | 255 | {"interface": "http", "limit": None, "optional": False}) | 291 | {"interface": "http", "limit": None, "optional": False, "scope": "global"}) |
713 | 256 | 292 | ||
714 | 257 | # defaults are properly applied | 293 | # defaults are properly applied |
715 | 258 | self.assertEqual( | 294 | self.assertEqual( |
716 | 259 | expander.coerce( | 295 | expander.coerce( |
717 | 260 | {"interface": "http"}, ["provides"]), | 296 | {"interface": "http"}, ["provides"]), |
719 | 261 | {"interface": "http", "limit": None, "optional": False}) | 297 | {"interface": "http", "limit": None, "optional": False, "scope": "global"}) |
720 | 262 | self.assertEqual( | 298 | self.assertEqual( |
721 | 263 | expander.coerce( | 299 | expander.coerce( |
722 | 264 | {"interface": "http", "limit": 2}, ["provides"]), | 300 | {"interface": "http", "limit": 2}, ["provides"]), |
724 | 265 | {"interface": "http", "limit": 2, "optional": False}) | 301 | {"interface": "http", "limit": 2, "optional": False, "scope": "global"}) |
725 | 266 | self.assertEqual( | 302 | self.assertEqual( |
726 | 267 | expander.coerce( | 303 | expander.coerce( |
728 | 268 | {"interface": "http", "optional": True}, | 304 | {"interface": "http", "optional": True, "scope": "global"}, |
729 | 269 | ["provides"]), | 305 | ["provides"]), |
731 | 270 | {"interface": "http", "limit": None, "optional": True}) | 306 | {"interface": "http", "limit": None, "optional": True, "scope": "global"}) |
732 | 271 | 307 | ||
733 | 272 | # invalid data raises SchemaError | 308 | # invalid data raises SchemaError |
734 | 273 | self.assertRaises( | 309 | self.assertRaises( |
735 | @@ -276,7 +312,7 @@ | |||
736 | 276 | self.assertRaises( | 312 | self.assertRaises( |
737 | 277 | SchemaError, | 313 | SchemaError, |
738 | 278 | expander.coerce, | 314 | expander.coerce, |
740 | 279 | {"interface": "http", "optional": None}, ["provides"]) | 315 | {"interface": "http", "optional": None, "scope": "global"}, ["provides"]) |
741 | 280 | self.assertRaises( | 316 | self.assertRaises( |
742 | 281 | SchemaError, | 317 | SchemaError, |
743 | 282 | expander.coerce, | 318 | expander.coerce, |
744 | @@ -286,4 +322,4 @@ | |||
745 | 286 | expander = InterfaceExpander(limit=1) | 322 | expander = InterfaceExpander(limit=1) |
746 | 287 | self.assertEqual( | 323 | self.assertEqual( |
747 | 288 | expander.coerce("http", ["consumes"]), | 324 | expander.coerce("http", ["consumes"]), |
749 | 289 | {"interface": "http", "limit": 1, "optional": False}) | 325 | {"interface": "http", "limit": 1, "optional": False, "scope": "global"}) |
As agreed and discussed in advance, the changes to the APIs and ZooKeeper handling of data
must be debated in advance in the mailing list.