Left: | ||
Right: |
OLD | NEW |
---|---|
1 import logging | 1 import logging |
2 import os | 2 import os |
3 | 3 |
4 import yaml | 4 import yaml |
5 | 5 |
6 from juju.charm.errors import MetaDataError | 6 from juju.charm.errors import MetaDataError |
7 from juju.errors import FileNotFound | 7 from juju.errors import FileNotFound |
8 from juju.lib.schema import ( | 8 from juju.lib.schema import ( |
9 SchemaError, Bool, Constant, Dict, Int, | 9 SchemaError, Bool, Constant, Dict, Int, |
10 KeyDict, OneOf, UnicodeOrString) | 10 KeyDict, OneOf, UnicodeOrString) |
11 | 11 |
12 | 12 |
13 log = logging.getLogger("juju.charm") | 13 log = logging.getLogger("juju.charm") |
14 | 14 |
15 | 15 |
16 UTF8_SCHEMA = UnicodeOrString("utf-8") | 16 UTF8_SCHEMA = UnicodeOrString("utf-8") |
17 | 17 |
18 SCOPE_GLOBAL = "global" | |
19 SCOPE_CONTAINER = "container" | |
20 | |
21 | |
18 INTERFACE_SCHEMA = KeyDict({ | 22 INTERFACE_SCHEMA = KeyDict({ |
19 "interface": UTF8_SCHEMA, | 23 "interface": UTF8_SCHEMA, |
20 "limit": OneOf(Constant(None), Int()), | 24 "limit": OneOf(Constant(None), Int()), |
21 "optional": Bool()}) | 25 "scope": OneOf(Constant(SCOPE_GLOBAL), Constant(SCOPE_CONTAINER)), |
26 "optional": Bool()}, | |
27 optional=["scope"]) | |
22 | 28 |
23 | 29 |
24 class InterfaceExpander(object): | 30 class InterfaceExpander(object): |
25 """Schema coercer that expands the interface shorthand notation. | 31 """Schema coercer that expands the interface shorthand notation. |
26 | 32 |
27 We need this class because our charm shorthand is difficult to | 33 We need this class because our charm shorthand is difficult to |
28 work with (unfortunately). So we coerce shorthand and then store | 34 work with (unfortunately). So we coerce shorthand and then store |
29 the desired format in ZK. | 35 the desired format in ZK. |
30 | 36 |
31 Supports the following variants:: | 37 Supports the following variants:: |
(...skipping 27 matching lines...) Expand all Loading... | |
59 | 65 |
60 Helper method to support each of the variants, either the | 66 Helper method to support each of the variants, either the |
61 charm does not specify limit and optional, such as foobar in | 67 charm does not specify limit and optional, such as foobar in |
62 the above example; or the interface spec is just a string, | 68 the above example; or the interface spec is just a string, |
63 such as the ``server: riak`` example. | 69 such as the ``server: riak`` example. |
64 """ | 70 """ |
65 if not isinstance(value, dict): | 71 if not isinstance(value, dict): |
66 return { | 72 return { |
67 "interface": UTF8_SCHEMA.coerce(value, path), | 73 "interface": UTF8_SCHEMA.coerce(value, path), |
68 "limit": self.limit, | 74 "limit": self.limit, |
75 "scope": "global", | |
69 "optional": False} | 76 "optional": False} |
70 else: | 77 else: |
71 # Optional values are context-sensitive and/or have | 78 # Optional values are context-sensitive and/or have |
72 # defaults, which is different than what KeyDict can | 79 # defaults, which is different than what KeyDict can |
73 # readily support. So just do it here first, then | 80 # readily support. So just do it here first, then |
74 # coerce. | 81 # coerce. |
75 if "limit" not in value: | 82 if "limit" not in value: |
76 value["limit"] = self.limit | 83 value["limit"] = self.limit |
77 if "optional" not in value: | 84 if "optional" not in value: |
78 value["optional"] = False | 85 value["optional"] = False |
86 value["scope"] = value.get("scope", "global") | |
79 return INTERFACE_SCHEMA.coerce(value, path) | 87 return INTERFACE_SCHEMA.coerce(value, path) |
80 | 88 |
81 | 89 |
82 SCHEMA = KeyDict({ | 90 SCHEMA = KeyDict({ |
83 "name": UTF8_SCHEMA, | 91 "name": UTF8_SCHEMA, |
84 "revision": Int(), | 92 "revision": Int(), |
85 "summary": UTF8_SCHEMA, | 93 "summary": UTF8_SCHEMA, |
86 "description": UTF8_SCHEMA, | 94 "description": UTF8_SCHEMA, |
87 "peers": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)), | 95 "peers": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)), |
88 "provides": Dict(UTF8_SCHEMA, InterfaceExpander(limit=None)), | 96 "provides": Dict(UTF8_SCHEMA, InterfaceExpander(limit=None)), |
89 "requires": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)), | 97 "requires": Dict(UTF8_SCHEMA, InterfaceExpander(limit=1)), |
90 }, optional=set(["provides", "requires", "peers", "revision"])) | 98 "subordinate": Bool(), |
99 }, optional=set(["provides", "requires", "peers", "revision", "subordinate"] )) | |
91 | 100 |
92 | 101 |
93 class MetaData(object): | 102 class MetaData(object): |
94 """Represents the charm info file. | 103 """Represents the charm info file. |
95 | 104 |
96 The main metadata for a charm (name, revision, etc) is maintained | 105 The main metadata for a charm (name, revision, etc) is maintained |
97 in the charm's info file. This class is able to parse, | 106 in the charm's info file. This class is able to parse, |
98 validate, and provide access to data in the info file. | 107 validate, and provide access to data in the info file. |
99 """ | 108 """ |
100 | 109 |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
137 @property | 146 @property |
138 def requires(self): | 147 def requires(self): |
139 """The charm requires relations.""" | 148 """The charm requires relations.""" |
140 return self._data.get("requires") | 149 return self._data.get("requires") |
141 | 150 |
142 @property | 151 @property |
143 def peers(self): | 152 def peers(self): |
144 """The charm peers relations.""" | 153 """The charm peers relations.""" |
145 return self._data.get("peers") | 154 return self._data.get("peers") |
146 | 155 |
156 @property | |
157 def is_subordinate(self): | |
158 """Indicates the charm requires a contained relationship. | |
159 | |
160 This property will effect the deployment options of its | |
161 charm. When a charm is_subordinate it can only be deployed | |
162 when its contained relationship is satisfied. See the | |
163 subordinates specification. | |
164 """ | |
165 if self._data.get("subordinate", False) is False: | |
166 return False | |
167 | |
168 if not self.requires: | |
169 return False | |
170 | |
171 for relation_data in self.requires.values(): | |
172 if relation_data.get("scope") == "container": | |
173 return True | |
174 return False | |
175 | |
147 def get_serialization_data(self): | 176 def get_serialization_data(self): |
148 """Get internal dictionary representing the state of this instance. | 177 """Get internal dictionary representing the state of this instance. |
149 | 178 |
150 This is useful to embed this information inside other storage-related | 179 This is useful to embed this information inside other storage-related |
151 dictionaries. | 180 dictionaries. |
152 """ | 181 """ |
153 return dict(self._data) | 182 return dict(self._data) |
154 | 183 |
155 def load(self, path): | 184 def load(self, path): |
156 """Load and parse the info file. | 185 """Load and parse the info file. |
(...skipping 23 matching lines...) Expand all Loading... | |
180 # Capture the path name on the error if present. | 209 # Capture the path name on the error if present. |
181 if path is not None: | 210 if path is not None: |
182 e.problem_mark.name = path | 211 e.problem_mark.name = path |
183 raise | 212 raise |
184 | 213 |
185 if "revision" in self._data and path: | 214 if "revision" in self._data and path: |
186 log.warning( | 215 log.warning( |
187 "%s: revision field is obsolete. Move it to the 'revision' " | 216 "%s: revision field is obsolete. Move it to the 'revision' " |
188 "file." % path) | 217 "file." % path) |
189 | 218 |
219 if self._data.get("subordinate", False) is True: | |
220 proper_subordinate = False | |
221 if self.requires: | |
222 for relation_data in self.requires.values(): | |
223 if relation_data.get("scope") == "container": | |
224 proper_subordinate = True | |
225 if not proper_subordinate: | |
226 log.warning( | |
227 "%s labeled subordinate but lacking scope:container `require s` relation", | |
228 path) | |
hazmat
2012/02/27 19:21:20
Feels like this should be an error that's raised.
bcsaller
2012/02/28 00:22:58
It isn't currently because the other checks in the
| |
229 | |
190 def parse_serialization_data(self, serialization_data, path=None): | 230 def parse_serialization_data(self, serialization_data, path=None): |
191 """Parse the unprocessed serialization data and load in this instance. | 231 """Parse the unprocessed serialization data and load in this instance. |
192 | 232 |
193 @param serialization_data: Unprocessed data matching the | 233 @param serialization_data: Unprocessed data matching the |
194 metadata schema. | 234 metadata schema. |
195 @param path: Optional path of the loaded file. Used when | 235 @param path: Optional path of the loaded file. Used when |
196 raising errors. | 236 raising errors. |
197 | 237 |
198 @raise MetaDataError: When errors are found in the info data. | 238 @raise MetaDataError: When errors are found in the info data. |
199 """ | 239 """ |
200 try: | 240 try: |
201 self._data = SCHEMA.coerce(serialization_data, []) | 241 self._data = SCHEMA.coerce(serialization_data, []) |
202 except SchemaError, error: | 242 except SchemaError, error: |
203 if path: | 243 if path: |
204 path_info = " %s:" % path | 244 path_info = " %s:" % path |
205 else: | 245 else: |
206 path_info = "" | 246 path_info = "" |
207 raise MetaDataError("Bad data in charm info:%s %s" % | 247 raise MetaDataError("Bad data in charm info:%s %s" % |
208 (path_info, error)) | 248 (path_info, error)) |
OLD | NEW |