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