Merge lp:~fwereade/juju-core/provider-skeleton into lp:~go-bot/juju-core/trunk
- provider-skeleton
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~fwereade/juju-core/provider-skeleton |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
1019 lines (+944/-4) 13 files modified
doc/hacking-providers.txt (+129/-0) provider/joyent/environ.go (+0/-4) provider/skeleton/config.go (+134/-0) provider/skeleton/config_test.go (+203/-0) provider/skeleton/environ.go (+119/-0) provider/skeleton/environ_firewall.go (+29/-0) provider/skeleton/environ_instance.go (+43/-0) provider/skeleton/export_test.go (+10/-0) provider/skeleton/instance.go (+50/-0) provider/skeleton/instance_firewall.go (+26/-0) provider/skeleton/provider.go (+121/-0) provider/skeleton/skeleton_test.go (+27/-0) provider/skeleton/storage.go (+53/-0) |
To merge this branch: | bzr merge lp:~fwereade/juju-core/provider-skeleton |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+189638@code.launchpad.net |
Commit message
Description of the change
skeleton provider
...and rudimentary initial implementation notes.
Roger Peppe (rogpeppe) wrote : | # |
This is great, thanks.
LGTM modulo the below comments and suggestions.
https:/
File doc/hacking-
https:/
doc/hacking-
defining how the substrate can be
s/basically//
https:/
doc/hacking-
environs.
I wonder if it might be nice to add some godoc links,
although it wouldn't improve text-only readability.
For example
http://
https:/
doc/hacking-
to enable easy mocking and hence allow
I'm not sure that exposing the support library as an interface
necessarily makes it much easier to mock. The interfaces can usually
be defined in the calling package.
https:/
doc/hacking-
of juju-core. Tooling support is spotty
Perhaps you could mention launchpad.
generate the file and can be used to update the dependencies too.
https:/
doc/hacking-
characters "skeleton" with your new provider name.
s/characters/word/ ?
https:/
File provider/
https:/
provider/
All the other providers put this on a new line and use [1:] at the end.
We should keep them consistent.
https:/
provider/
with sensitive values.
Let's use full sentences here, to fit with the changes made in
https:/
and four-space indents likewise.
https:/
provider/
Please let's have doc comments on all variable and function
definitions, as it's important that people understand the
purpose of each part of the skeleton they're supposed to
be adapting.
https:/
provider/
will be inserted if necessary. If the
s/validated/
https:/
provider/
continuing.
s/,//
https:/
provider...
Kapil Thangavelu (hazmat) wrote : | # |
it would be great if this could resurrected and pushed for people starting in on new providers.
John A Meinel (jameinel) wrote : | # |
I'm pretty sure we wanted to land this, is there anything we can do to help it along?
- 1901. By William Reade
-
merge parent
- 1902. By William Reade
-
now satisifes Environ interface again
Unmerged revisions
- 1902. By William Reade
-
now satisifes Environ interface again
- 1901. By William Reade
-
merge parent
- 1900. By William Reade
-
drop overly-rough doc bits
- 1899. By William Reade
-
add comments
- 1898. By William Reade
-
added sample config tests
- 1897. By William Reade
-
merge parent
- 1896. By William Reade
-
finish Prepare logic
- 1895. By William Reade
-
merge parent
- 1894. By William Reade
-
basic skeleton provider, docs in progress
- 1893. By William Reade
-
merge parent
Preview Diff
1 | === added file 'doc/hacking-providers.txt' |
2 | --- doc/hacking-providers.txt 1970-01-01 00:00:00 +0000 |
3 | +++ doc/hacking-providers.txt 2014-04-18 14:48:42 +0000 |
4 | @@ -0,0 +1,129 @@ |
5 | + |
6 | + |
7 | +Juju providers |
8 | +-------------- |
9 | + |
10 | +Juju currently allows you to deploy and scale services on deveral different |
11 | +kinds of substrate, including MAAS, Azure, AWS, and a variety of Openstack clouds |
12 | +(including HPCloud). To add a new kind of substrate, you need to add a new |
13 | +"provider" to juju; this basically involves defining how the substrate can be |
14 | +configured, and writing the code that supports the (relatively few) provider |
15 | +operations that juju needs to know. |
16 | + |
17 | + |
18 | +Required operations |
19 | +------------------- |
20 | + |
21 | +Juju requires that a cloud provider support the following operations: |
22 | + |
23 | + * start a fresh instance (Environ.StartInstance) |
24 | + * running a particular series of Ubuntu |
25 | + * with foreknowledge of what processor architecture it will run |
26 | + * with a customized cloud-init script |
27 | + * ...and report the new instance's hardware characteristics |
28 | + * query the list of running instances (Environ.Instances, Environ.AllInstances) |
29 | + * stop a running instance (Environ.StopInstances) |
30 | + * discover a running instances's addresses (Instance.Addresses) |
31 | + |
32 | +...which roughly correspond to the environs.InstanceBroker interface. |
33 | + |
34 | +To enable the benefits of containerization for your cloud provider, you will need |
35 | +some way to create new internal addresses and assign them to instances. The actual |
36 | +interface for this is still under discussion. |
37 | + |
38 | +Juju strongly recommends that a cloud provider support the following operations: |
39 | + |
40 | + * read names in object storage (StorageReader.List) |
41 | + * read a named file from object storage (StorageReader.Get) |
42 | + * make a named file from object storage available over HTTP (StorageReader.URL) |
43 | + * write a named file to object storage (StorageWriter.Put) |
44 | + * remove a named file from object storage (StorageWriter.Remove) |
45 | + |
46 | +...but if you can't do storage, come and talk to us: there are potential ways |
47 | +round this, and by the time you read this they may be a reality already. But it |
48 | +would not be wise to implement in-environment storage without discussion. |
49 | + |
50 | +Finally, if the cloud provider supports them, juju appreciates support for the |
51 | +following groups of operations: |
52 | + |
53 | + * open a port for every machine in an environment (Environ.OpenPorts) |
54 | + * list open ports for every machine in an environment (Environ.Ports) |
55 | + * close a port for every machine in an environment (Environ.ClosePorts) |
56 | + |
57 | + * open a port for one machine in an environment (Instance.OpenPorts) |
58 | + * list open ports for one machine in an environment (Instance.Ports) |
59 | + * close a port for one machine in an environment (Instance.ClosePorts) |
60 | + |
61 | +...but if you can't support those, don't worry: just make the methods return without |
62 | +error and juju will make do. |
63 | + |
64 | +If your cloud doesn't assign a public address for every new instance, you will |
65 | +almost certainly need to do so for the bootstrap node (so that the client can |
66 | +communicate with the API); and will most likely need to implement any OpenPorts |
67 | +methods such that they ensure the target instance(s) have public addresses |
68 | +assigned. |
69 | + |
70 | + |
71 | +Implementing the required operations |
72 | +------------------------------------ |
73 | + |
74 | +We very strongly recommend that you implement a library layer exposing the above |
75 | +before attempting to write a provider for juju. Mixing the low-level concerns of |
76 | +talking to your API into the provider -- which is essentially an adapter layer -- |
77 | +is a recipe for trouble. The following packages may serve as useful inspiration; |
78 | +all were written for, and are used by, juju providers. |
79 | + |
80 | + * launchpad.net/goamz (ec2 provider) |
81 | + * launchpad.net/gwacl (azure provider) |
82 | + * launchpad.net/gomaasapi (maas provider) |
83 | + * launchpad.net/goose (openstack provider) |
84 | + |
85 | +We have in the past tried to write large-scale test doubles against which common |
86 | +provider tests can run within juju, but have found in practice that this approach |
87 | +is often difficult to work with effectively. We therefore now suggest that your |
88 | +support library be exposed as interfaces, to enable easy mocking and hence allow |
89 | +for comprehensive unit testing of the provider package. |
90 | + |
91 | +We have a separate team working on cross-provider functional tests for juju. To |
92 | +arrange automatic testing against your cloud, please get in touch. |
93 | + |
94 | + |
95 | +Dependency management |
96 | +--------------------- |
97 | + |
98 | +Dependency management can be problematic in go. Despite the advice above, library |
99 | +development tends to proceed in parallel with provider development; and when this |
100 | +is happening, it's really unpleasant having to manage compatibility. The glib |
101 | +answer is to stipulate that you'll only make backward-compatible changes to your |
102 | +support library, and will publish a new version at a new URL whenever you make a |
103 | +breaking change; but in our experience this does not work well in practice. Even |
104 | +when the same party controls both sides, it's all too easy to break in surprising |
105 | +ways. |
106 | + |
107 | +So we have "dependencies.tsv" in the root of juju-core. Tooling support is spotty |
108 | +but improving, and if you keep it up to date you should be free at least to |
109 | +change your support library without breaking juju-core. Other clients are, sad |
110 | +to say, on their own. |
111 | + |
112 | + |
113 | +Implementing an actual provider |
114 | +------------------------------- |
115 | + |
116 | +Start by copying the "providers/skeleton" package into a new subpackage of "providers", |
117 | +and replace every instance of the characters "skeleton" with your new provider name. |
118 | + |
119 | +Then take a look at "config.go", and change or remove the example fields to suit your |
120 | +new provider. There will probably be at least one field holding credentials, that |
121 | +should replace "skeleton-secret-field"; and there may be other fields that should be |
122 | +handled similarly to "skeleton-default-field" and/or "skeleton-immutable-field"; for |
123 | +example, a "region" setting should probably have a default value and should almost |
124 | +certainly not be mutable for a given environment. |
125 | + |
126 | +Don't forget to keep the config tests comprehensive and up to date. |
127 | + |
128 | +Once you've got a working config, you should be in a position to start trying to |
129 | +bootstrap the environment, and implementing methods as they fail. To begin with, |
130 | +you'll definitely need to implement (much of) the Storage type; then you'll need |
131 | +Environ.StartInstance, and Environ.Instances; and then the Instance type. |
132 | + |
133 | +To Be Continued... |
134 | |
135 | === modified file 'provider/joyent/environ.go' |
136 | --- provider/joyent/environ.go 2014-04-09 16:36:12 +0000 |
137 | +++ provider/joyent/environ.go 2014-04-18 14:48:42 +0000 |
138 | @@ -128,10 +128,6 @@ |
139 | return env.getSnapshot().storage |
140 | } |
141 | |
142 | -func (env *joyentEnviron) PublicStorage() storage.StorageReader { |
143 | - return environs.EmptyStorage |
144 | -} |
145 | - |
146 | func (env *joyentEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
147 | return common.Bootstrap(ctx, env, cons) |
148 | } |
149 | |
150 | === added directory 'provider/skeleton' |
151 | === added file 'provider/skeleton/config.go' |
152 | --- provider/skeleton/config.go 1970-01-01 00:00:00 +0000 |
153 | +++ provider/skeleton/config.go 2014-04-18 14:48:42 +0000 |
154 | @@ -0,0 +1,134 @@ |
155 | +// Copyright 2013 Canonical Ltd. |
156 | +// Licensed under the AGPLv3, see LICENCE file for details. |
157 | + |
158 | +package skeleton |
159 | + |
160 | +import ( |
161 | + "fmt" |
162 | + |
163 | + "launchpad.net/juju-core/environs/config" |
164 | + "launchpad.net/juju-core/schema" |
165 | + "launchpad.net/juju-core/utils" |
166 | +) |
167 | + |
168 | +// boilerplateConfig will be shown in help output, so please keep it up to |
169 | +// date when you change environment configuration below. |
170 | +const boilerplateConfig = `skeleton: |
171 | + type: skeleton |
172 | + |
173 | + # this exists to demonstrate how to deal with sensitive values. |
174 | + skeleton-secret-field: <cloud credentials, for example> |
175 | + |
176 | + # this exists to demonstrate how to deal with values that can't change; and |
177 | + # also how to use Prepare to fill in values that don't makes sense with |
178 | + # static defaults but can be inferred or chosen at runtime. |
179 | + # skeleton-immutable-field: <a storage bucket name, for example> |
180 | + |
181 | + # this exists to demonstrate how to deal with static default values that |
182 | + # some users may wish to override |
183 | + # skeleton-default-field: <specific default value> |
184 | + |
185 | +` |
186 | + |
187 | +var configFields = schema.Fields{ |
188 | + "skeleton-secret-field": schema.String(), |
189 | + "skeleton-immutable-field": schema.String(), |
190 | + "skeleton-default-field": schema.String(), |
191 | +} |
192 | + |
193 | +var configDefaultFields = schema.Defaults{ |
194 | + "skeleton-default-field": "<specific default value>", |
195 | +} |
196 | + |
197 | +var configSecretFields = []string{ |
198 | + "skeleton-secret-field", |
199 | +} |
200 | + |
201 | +var configImmutableFields = []string{ |
202 | + "skeleton-immutable-field", |
203 | +} |
204 | + |
205 | +func prepareConfig(cfg *config.Config) (*config.Config, error) { |
206 | + // Turn an incomplete config into a valid one, if possible. |
207 | + attrs := cfg.UnknownAttrs() |
208 | + if _, ok := attrs["skeleton-immutable-field"]; !ok { |
209 | + uuid, err := utils.NewUUID() |
210 | + if err != nil { |
211 | + return nil, fmt.Errorf("cannot generate skeleton-immutable-field") |
212 | + } |
213 | + attrs["skeleton-immutable-field"] = fmt.Sprintf("%x", uuid.Raw()) |
214 | + } |
215 | + return cfg.Apply(attrs) |
216 | +} |
217 | + |
218 | +func validateConfig(cfg *config.Config, old *environConfig) (*environConfig, error) { |
219 | + // Check sanity of juju-level fields. |
220 | + var oldCfg *config.Config |
221 | + if old != nil { |
222 | + oldCfg = old.Config |
223 | + } |
224 | + if err := config.Validate(cfg, oldCfg); err != nil { |
225 | + return nil, err |
226 | + } |
227 | + |
228 | + // Extract validated provider-specific fields. All of configFields will be |
229 | + // present in validated, and defaults will be inserted if necessary. If the |
230 | + // schema you passed in doesn't quite express what you need, you can make |
231 | + // whatever checks you need here, before continuing. |
232 | + // In particular, if you want to extract (say) credentials from the user's |
233 | + // shell environment variables, you'll need to allow missing values to pass |
234 | + // through the schema by setting a value of schema.Omit in the configFields |
235 | + // map, and then to set and check them at this point. These values *must* be |
236 | + // stored in newAttrs: a Config will be generated on the user's machine only |
237 | + // to begin with, and will subsequently be used on a different machine that |
238 | + // will probably not have those variables set. |
239 | + newAttrs, err := cfg.ValidateUnknownAttrs(configFields, configDefaultFields) |
240 | + if err != nil { |
241 | + return nil, err |
242 | + } |
243 | + for field := range configFields { |
244 | + if newAttrs[field] == "" { |
245 | + return nil, fmt.Errorf("%s: must not be empty", field) |
246 | + } |
247 | + } |
248 | + |
249 | + // If an old config was supplied, check any immutable fields have not changed. |
250 | + if old != nil { |
251 | + for _, field := range configImmutableFields { |
252 | + if old.attrs[field] != newAttrs[field] { |
253 | + return nil, fmt.Errorf( |
254 | + "%s: cannot change from %v to %v", |
255 | + field, old.attrs[field], newAttrs[field], |
256 | + ) |
257 | + } |
258 | + } |
259 | + } |
260 | + |
261 | + // Merge the validated provider-specific fields into the original config, |
262 | + // to ensure the object we return is internally consistent. |
263 | + newCfg, err := cfg.Apply(newAttrs) |
264 | + if err != nil { |
265 | + return nil, err |
266 | + } |
267 | + return &environConfig{ |
268 | + Config: newCfg, |
269 | + attrs: newAttrs, |
270 | + }, nil |
271 | +} |
272 | + |
273 | +type environConfig struct { |
274 | + *config.Config |
275 | + attrs map[string]interface{} |
276 | +} |
277 | + |
278 | +func (ecfg *environConfig) secretField() string { |
279 | + return ecfg.attrs["skeleton-secret-field"].(string) |
280 | +} |
281 | + |
282 | +func (ecfg *environConfig) immutableField() string { |
283 | + return ecfg.attrs["skeleton-immutable-field"].(string) |
284 | +} |
285 | + |
286 | +func (ecfg *environConfig) defaultField() string { |
287 | + return ecfg.attrs["skeleton-default-field"].(string) |
288 | +} |
289 | |
290 | === added file 'provider/skeleton/config_test.go' |
291 | --- provider/skeleton/config_test.go 1970-01-01 00:00:00 +0000 |
292 | +++ provider/skeleton/config_test.go 2014-04-18 14:48:42 +0000 |
293 | @@ -0,0 +1,203 @@ |
294 | +// Copyright 2013 Canonical Ltd. |
295 | +// Licensed under the AGPLv3, see LICENCE file for details. |
296 | + |
297 | +package skeleton_test |
298 | + |
299 | +import ( |
300 | + gc "launchpad.net/gocheck" |
301 | + |
302 | + "launchpad.net/juju-core/environs" |
303 | + "launchpad.net/juju-core/environs/config" |
304 | + "launchpad.net/juju-core/provider/skeleton" |
305 | + "launchpad.net/juju-core/testing" |
306 | + "launchpad.net/juju-core/testing/testbase" |
307 | +) |
308 | + |
309 | +func newConfig(c *gc.C, attrs testing.Attrs) *config.Config { |
310 | + attrs = testing.FakeConfig().Merge(attrs) |
311 | + cfg, err := config.New(config.NoDefaults, attrs) |
312 | + c.Assert(err, gc.IsNil) |
313 | + return cfg |
314 | +} |
315 | + |
316 | +func validAttrs() testing.Attrs { |
317 | + return testing.FakeConfig().Merge(testing.Attrs{ |
318 | + "type": "skeleton", |
319 | + "skeleton-secret-field": "seekrit", |
320 | + "skeleton-immutable-field": "static", |
321 | + }) |
322 | +} |
323 | + |
324 | +type ConfigSuite struct { |
325 | + testbase.LoggingSuite |
326 | +} |
327 | + |
328 | +var _ = gc.Suite(&ConfigSuite{}) |
329 | + |
330 | +var newConfigTests = []struct { |
331 | + info string |
332 | + insert testing.Attrs |
333 | + remove []string |
334 | + expect testing.Attrs |
335 | + err string |
336 | +}{{ |
337 | + info: "skeleton-immutable-field is required", |
338 | + remove: []string{"skeleton-immutable-field"}, |
339 | + err: "skeleton-immutable-field: expected string, got nothing", |
340 | +}, { |
341 | + info: "skeleton-immutable-field cannot be empty", |
342 | + insert: testing.Attrs{"skeleton-immutable-field": ""}, |
343 | + err: "skeleton-immutable-field: must not be empty", |
344 | +}, { |
345 | + info: "skeleton-secret-field is required", |
346 | + remove: []string{"skeleton-secret-field"}, |
347 | + err: "skeleton-secret-field: expected string, got nothing", |
348 | +}, { |
349 | + info: "skeleton-secret-field cannot be empty", |
350 | + insert: testing.Attrs{"skeleton-secret-field": ""}, |
351 | + err: "skeleton-secret-field: must not be empty", |
352 | +}, { |
353 | + info: "skeleton-default-field is inserted if missing", |
354 | + expect: testing.Attrs{"skeleton-default-field": "<specific default value>"}, |
355 | +}, { |
356 | + info: "skeleton-default-field cannot be empty", |
357 | + insert: testing.Attrs{"skeleton-default-field": ""}, |
358 | + err: "skeleton-default-field: must not be empty", |
359 | +}, { |
360 | + info: "skeleton-default-field is untouched if present", |
361 | + insert: testing.Attrs{"skeleton-default-field": "<user value>"}, |
362 | + expect: testing.Attrs{"skeleton-default-field": "<user value>"}, |
363 | +}, { |
364 | + info: "unknown field is not touched", |
365 | + insert: testing.Attrs{"unknown-field": 12345}, |
366 | + expect: testing.Attrs{"unknown-field": 12345}, |
367 | +}} |
368 | + |
369 | +func (*ConfigSuite) TestNewEnvironConfig(c *gc.C) { |
370 | + for i, test := range newConfigTests { |
371 | + c.Logf("test %d: %s", i, test.info) |
372 | + attrs := validAttrs().Merge(test.insert).Delete(test.remove...) |
373 | + testConfig := newConfig(c, attrs) |
374 | + environ, err := environs.New(testConfig) |
375 | + if test.err == "" { |
376 | + c.Check(err, gc.IsNil) |
377 | + attrs := environ.Config().AllAttrs() |
378 | + for field, value := range test.expect { |
379 | + c.Check(attrs[field], gc.Equals, value) |
380 | + } |
381 | + } else { |
382 | + c.Check(environ, gc.IsNil) |
383 | + c.Check(err, gc.ErrorMatches, test.err) |
384 | + } |
385 | + } |
386 | +} |
387 | + |
388 | +func (*ConfigSuite) TestValidateNewConfig(c *gc.C) { |
389 | + for i, test := range newConfigTests { |
390 | + c.Logf("test %d: %s", i, test.info) |
391 | + attrs := validAttrs().Merge(test.insert).Delete(test.remove...) |
392 | + testConfig := newConfig(c, attrs) |
393 | + validatedConfig, err := skeleton.Provider.Validate(testConfig, nil) |
394 | + if test.err == "" { |
395 | + c.Check(err, gc.IsNil) |
396 | + attrs := validatedConfig.AllAttrs() |
397 | + for field, value := range test.expect { |
398 | + c.Check(attrs[field], gc.Equals, value) |
399 | + } |
400 | + } else { |
401 | + c.Check(validatedConfig, gc.IsNil) |
402 | + c.Check(err, gc.ErrorMatches, "invalid config: "+test.err) |
403 | + } |
404 | + } |
405 | +} |
406 | + |
407 | +func (*ConfigSuite) TestValidateOldConfig(c *gc.C) { |
408 | + knownGoodConfig := newConfig(c, validAttrs()) |
409 | + for i, test := range newConfigTests { |
410 | + c.Logf("test %d: %s", i, test.info) |
411 | + attrs := validAttrs().Merge(test.insert).Delete(test.remove...) |
412 | + testConfig := newConfig(c, attrs) |
413 | + validatedConfig, err := skeleton.Provider.Validate(knownGoodConfig, testConfig) |
414 | + if test.err == "" { |
415 | + c.Check(err, gc.IsNil) |
416 | + attrs := validatedConfig.AllAttrs() |
417 | + for field, value := range validAttrs() { |
418 | + c.Check(attrs[field], gc.Equals, value) |
419 | + } |
420 | + } else { |
421 | + c.Check(validatedConfig, gc.IsNil) |
422 | + c.Check(err, gc.ErrorMatches, "invalid base config: "+test.err) |
423 | + } |
424 | + } |
425 | +} |
426 | + |
427 | +var changeConfigTests = []struct { |
428 | + info string |
429 | + insert testing.Attrs |
430 | + remove []string |
431 | + expect testing.Attrs |
432 | + err string |
433 | +}{{ |
434 | + info: "no change, no error", |
435 | + expect: validAttrs(), |
436 | +}, { |
437 | + info: "can change skeleton-secret-field", |
438 | + insert: testing.Attrs{"skeleton-secret-field": "okkult"}, |
439 | + expect: testing.Attrs{"skeleton-secret-field": "okkult"}, |
440 | +}, { |
441 | + info: "can change skeleton-default-field", |
442 | + insert: testing.Attrs{"skeleton-default-field": "different"}, |
443 | + expect: testing.Attrs{"skeleton-default-field": "different"}, |
444 | +}, { |
445 | + info: "cannot change skeleton-immutable-field", |
446 | + insert: testing.Attrs{"skeleton-immutable-field": "mutant"}, |
447 | + err: "skeleton-immutable-field: cannot change from static to mutant", |
448 | +}, { |
449 | + info: "can insert unknown field", |
450 | + insert: testing.Attrs{"unknown": "ignoti"}, |
451 | + expect: testing.Attrs{"unknown": "ignoti"}, |
452 | +}} |
453 | + |
454 | +func (s *ConfigSuite) TestValidateChange(c *gc.C) { |
455 | + baseConfig := newConfig(c, validAttrs()) |
456 | + for i, test := range changeConfigTests { |
457 | + c.Logf("test %d: %s", i, test.info) |
458 | + attrs := validAttrs().Merge(test.insert).Delete(test.remove...) |
459 | + testConfig := newConfig(c, attrs) |
460 | + validatedConfig, err := skeleton.Provider.Validate(testConfig, baseConfig) |
461 | + if test.err == "" { |
462 | + c.Check(err, gc.IsNil) |
463 | + attrs := validatedConfig.AllAttrs() |
464 | + for field, value := range test.expect { |
465 | + c.Check(attrs[field], gc.Equals, value) |
466 | + } |
467 | + } else { |
468 | + c.Check(validatedConfig, gc.IsNil) |
469 | + c.Check(err, gc.ErrorMatches, "invalid config change: "+test.err) |
470 | + } |
471 | + } |
472 | +} |
473 | + |
474 | +func (s *ConfigSuite) TestSetConfig(c *gc.C) { |
475 | + baseConfig := newConfig(c, validAttrs()) |
476 | + for i, test := range changeConfigTests { |
477 | + c.Logf("test %d: %s", i, test.info) |
478 | + environ, err := environs.New(baseConfig) |
479 | + c.Assert(err, gc.IsNil) |
480 | + attrs := validAttrs().Merge(test.insert).Delete(test.remove...) |
481 | + testConfig := newConfig(c, attrs) |
482 | + err = environ.SetConfig(testConfig) |
483 | + newAttrs := environ.Config().AllAttrs() |
484 | + if test.err == "" { |
485 | + c.Check(err, gc.IsNil) |
486 | + for field, value := range test.expect { |
487 | + c.Check(newAttrs[field], gc.Equals, value) |
488 | + } |
489 | + } else { |
490 | + c.Check(err, gc.ErrorMatches, test.err) |
491 | + for field, value := range baseConfig.UnknownAttrs() { |
492 | + c.Check(newAttrs[field], gc.Equals, value) |
493 | + } |
494 | + } |
495 | + } |
496 | +} |
497 | |
498 | === added file 'provider/skeleton/environ.go' |
499 | --- provider/skeleton/environ.go 1970-01-01 00:00:00 +0000 |
500 | +++ provider/skeleton/environ.go 2014-04-18 14:48:42 +0000 |
501 | @@ -0,0 +1,119 @@ |
502 | +// Copyright 2013 Canonical Ltd. |
503 | +// Licensed under the AGPLv3, see LICENCE file for details. |
504 | + |
505 | +package skeleton |
506 | + |
507 | +import ( |
508 | + "sync" |
509 | + |
510 | + "launchpad.net/juju-core/constraints" |
511 | + "launchpad.net/juju-core/environs" |
512 | + "launchpad.net/juju-core/environs/config" |
513 | + "launchpad.net/juju-core/environs/storage" |
514 | + "launchpad.net/juju-core/juju/arch" |
515 | + "launchpad.net/juju-core/provider/common" |
516 | + "launchpad.net/juju-core/state" |
517 | + "launchpad.net/juju-core/state/api" |
518 | +) |
519 | + |
520 | +// This file contains the core of the skeleton Environ implementation. You will |
521 | +// probably not need to change this file very much to begin with; and if you |
522 | +// never need to add any more fields, you may never need to touch it. |
523 | +// |
524 | +// The rest of the implementation is split into environ_instance.go (which |
525 | +// must be implemented ) and environ_firewall.go (which can be safely |
526 | +// ignored until you've got an environment bootstrapping successfully). |
527 | + |
528 | +type environ struct { |
529 | + // This is used to check sanity of provisioning requests ahead of time. The |
530 | + // default implementation doesn't check anything; an ideal environ will use |
531 | + // its own PrecheckInstance method to prevent impossible provisioning |
532 | + // requests before they're made. |
533 | + common.NopPrecheckerPolicy |
534 | + |
535 | + // The SupportsUnitPlacementPolicy makes unit placement available on the |
536 | + // provider. The only reason to replace it would be if you were implementing |
537 | + // a provider like azure, in which we had to sacrifice unit placement in |
538 | + // favour of making it possible to keep services highly available. |
539 | + common.SupportsUnitPlacementPolicy |
540 | + |
541 | + name string |
542 | + // All mutating operations should lock the mutex. Non-mutating operations |
543 | + // should read all fields (other than name, which is immutable) from a |
544 | + // shallow copy taken with getSnapshot(). |
545 | + // This advice is predicated on the goroutine-safety of the values of the |
546 | + // affected fields. |
547 | + lock sync.Mutex |
548 | + ecfg *environConfig |
549 | + storage storage.Storage |
550 | +} |
551 | + |
552 | +var _ environs.Environ = (*environ)(nil) |
553 | + |
554 | +func (env *environ) Name() string { |
555 | + return env.name |
556 | +} |
557 | + |
558 | +func (*environ) Provider() environs.EnvironProvider { |
559 | + return providerInstance |
560 | +} |
561 | + |
562 | +func (env *environ) SetConfig(cfg *config.Config) error { |
563 | + env.lock.Lock() |
564 | + defer env.lock.Unlock() |
565 | + ecfg, err := validateConfig(cfg, env.ecfg) |
566 | + if err != nil { |
567 | + return err |
568 | + } |
569 | + storage, err := newStorage(ecfg) |
570 | + if err != nil { |
571 | + return err |
572 | + } |
573 | + env.ecfg = ecfg |
574 | + env.storage = storage |
575 | + return nil |
576 | +} |
577 | + |
578 | +func (env *environ) getSnapshot() *environ { |
579 | + env.lock.Lock() |
580 | + clone := *env |
581 | + env.lock.Unlock() |
582 | + clone.lock = sync.Mutex{} |
583 | + return &clone |
584 | +} |
585 | + |
586 | +func (env *environ) Config() *config.Config { |
587 | + return env.getSnapshot().ecfg.Config |
588 | +} |
589 | + |
590 | +func (env *environ) Storage() storage.Storage { |
591 | + return env.getSnapshot().storage |
592 | +} |
593 | + |
594 | +func (env *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
595 | + // You can probably ignore this method; the common implementation should work. |
596 | + return common.Bootstrap(ctx, env, cons) |
597 | +} |
598 | + |
599 | +func (env *environ) StateInfo() (*state.Info, *api.Info, error) { |
600 | + // You can probably ignore this method; the common implementation should work. |
601 | + return common.StateInfo(env) |
602 | +} |
603 | + |
604 | +func (env *environ) Destroy() error { |
605 | + // You can probably ignore this method; the common implementation should work. |
606 | + return common.Destroy(env) |
607 | +} |
608 | + |
609 | +// SupportedArchitectures is specified on the EnvironCapability interface. |
610 | +func (env *environ) SupportedArchitectures() ([]string, error) { |
611 | + // An ideal implementation will inspect the tools, images, and instance types |
612 | + // available in the environment to return correct values here. |
613 | + return arch.AllSupportedArches, nil |
614 | +} |
615 | + |
616 | +// SupportNetworks is specified on the EnvironCapability interface. |
617 | +func (env *environ) SupportNetworks() bool { |
618 | + // An ideal implementation will support networking. |
619 | + return false |
620 | +} |
621 | |
622 | === added file 'provider/skeleton/environ_firewall.go' |
623 | --- provider/skeleton/environ_firewall.go 1970-01-01 00:00:00 +0000 |
624 | +++ provider/skeleton/environ_firewall.go 2014-04-18 14:48:42 +0000 |
625 | @@ -0,0 +1,29 @@ |
626 | +// Copyright 2013 Canonical Ltd. |
627 | +// Licensed under the AGPLv3, see LICENCE file for details. |
628 | + |
629 | +package skeleton |
630 | + |
631 | +import ( |
632 | + "launchpad.net/juju-core/instance" |
633 | +) |
634 | + |
635 | +// Implementing the methods below (to do something other than return nil) will |
636 | +// cause `juju expose` to work when the firewall-mode is "global". If you |
637 | +// implement one of them, you should implement them all. |
638 | + |
639 | +func (env *environ) OpenPorts(ports []instance.Port) error { |
640 | + logger.Warningf("pretending to open ports %v for all instances", ports) |
641 | + _ = env.getSnapshot() |
642 | + return nil |
643 | +} |
644 | + |
645 | +func (env *environ) ClosePorts(ports []instance.Port) error { |
646 | + logger.Warningf("pretending to close ports %v for all instances", ports) |
647 | + _ = env.getSnapshot() |
648 | + return nil |
649 | +} |
650 | + |
651 | +func (env *environ) Ports() ([]instance.Port, error) { |
652 | + _ = env.getSnapshot() |
653 | + return nil, nil |
654 | +} |
655 | |
656 | === added file 'provider/skeleton/environ_instance.go' |
657 | --- provider/skeleton/environ_instance.go 1970-01-01 00:00:00 +0000 |
658 | +++ provider/skeleton/environ_instance.go 2014-04-18 14:48:42 +0000 |
659 | @@ -0,0 +1,43 @@ |
660 | +// Copyright 2013 Canonical Ltd. |
661 | +// Licensed under the AGPLv3, see LICENCE file for details. |
662 | + |
663 | +package skeleton |
664 | + |
665 | +import ( |
666 | + "launchpad.net/juju-core/environs" |
667 | + "launchpad.net/juju-core/instance" |
668 | +) |
669 | + |
670 | +func (env *environ) StartInstance(args environs.StartInstanceParams) ( |
671 | + instance.Instance, *instance.HardwareCharacteristics, []environs.NetworkInfo, error, |
672 | +) { |
673 | + // Please note that in order to fulfil the demands made of Instances and |
674 | + // AllInstances, it is imperative that some environment feature be used to |
675 | + // keep track of which instances were actually started by juju. |
676 | + _ = env.getSnapshot() |
677 | + return nil, nil, nil, errNotImplemented |
678 | +} |
679 | + |
680 | +func (env *environ) AllInstances() ([]instance.Instance, error) { |
681 | + // Please note that this must *not* return instances that have not been |
682 | + // allocated as part of this environment -- if it does, juju will see they |
683 | + // are not tracked in state, assume they're stale/rogue, and shut them down. |
684 | + _ = env.getSnapshot() |
685 | + return nil, errNotImplemented |
686 | +} |
687 | + |
688 | +func (env *environ) Instances(ids []instance.Id) ([]instance.Instance, error) { |
689 | + // Please note that this must *not* return instances that have not been |
690 | + // allocated as part of this environment -- if it does, juju will see they |
691 | + // are not tracked in state, assume they're stale/rogue, and shut them down. |
692 | + // This advice applies even if an instance id passed in corresponds to a |
693 | + // real instance that's not part of the environment -- the Environ should |
694 | + // treat that no differently to a request for one that does not exist. |
695 | + _ = env.getSnapshot() |
696 | + return nil, errNotImplemented |
697 | +} |
698 | + |
699 | +func (env *environ) StopInstances(instances []instance.Instance) error { |
700 | + _ = env.getSnapshot() |
701 | + return errNotImplemented |
702 | +} |
703 | |
704 | === added file 'provider/skeleton/export_test.go' |
705 | --- provider/skeleton/export_test.go 1970-01-01 00:00:00 +0000 |
706 | +++ provider/skeleton/export_test.go 2014-04-18 14:48:42 +0000 |
707 | @@ -0,0 +1,10 @@ |
708 | +// Copyright 2013 Canonical Ltd. |
709 | +// Licensed under the AGPLv3, see LICENCE file for details. |
710 | + |
711 | +package skeleton |
712 | + |
713 | +import ( |
714 | + "launchpad.net/juju-core/environs" |
715 | +) |
716 | + |
717 | +var Provider environs.EnvironProvider = providerInstance |
718 | |
719 | === added file 'provider/skeleton/instance.go' |
720 | --- provider/skeleton/instance.go 1970-01-01 00:00:00 +0000 |
721 | +++ provider/skeleton/instance.go 2014-04-18 14:48:42 +0000 |
722 | @@ -0,0 +1,50 @@ |
723 | +// Copyright 2013 Canonical Ltd. |
724 | +// Licensed under the AGPLv3, see LICENCE file for details. |
725 | + |
726 | +package skeleton |
727 | + |
728 | +import ( |
729 | + "launchpad.net/juju-core/instance" |
730 | + "launchpad.net/juju-core/provider/common" |
731 | +) |
732 | + |
733 | +type environInstance struct { |
734 | + id instance.Id |
735 | + env *environ |
736 | +} |
737 | + |
738 | +var _ instance.Instance = (*environInstance)(nil) |
739 | + |
740 | +func (inst *environInstance) Id() instance.Id { |
741 | + return inst.id |
742 | +} |
743 | + |
744 | +func (inst *environInstance) Status() string { |
745 | + _ = inst.env.getSnapshot() |
746 | + return "unknown (not implemented)" |
747 | +} |
748 | + |
749 | +func (inst *environInstance) Refresh() error { |
750 | + _ = inst.env.getSnapshot() |
751 | + return errNotImplemented |
752 | +} |
753 | + |
754 | +func (inst *environInstance) Addresses() ([]instance.Address, error) { |
755 | + _ = inst.env.getSnapshot() |
756 | + return nil, errNotImplemented |
757 | +} |
758 | + |
759 | +func (inst *environInstance) DNSName() (string, error) { |
760 | + // This method is likely to be replaced entirely by Addresses() at some point, |
761 | + // but remains necessary for now. It's probably smart to implement it in |
762 | + // terms of Addresses above, to minimise churn when it's removed. |
763 | + _ = inst.env.getSnapshot() |
764 | + return "", errNotImplemented |
765 | +} |
766 | + |
767 | +func (inst *environInstance) WaitDNSName() (string, error) { |
768 | + // This method is likely to be replaced entirely by Addresses() at some point, |
769 | + // but remains necessary for now. Until it's finally removed, you can probably |
770 | + // ignore this method; the common implementation should work. |
771 | + return common.WaitDNSName(inst) |
772 | +} |
773 | |
774 | === added file 'provider/skeleton/instance_firewall.go' |
775 | --- provider/skeleton/instance_firewall.go 1970-01-01 00:00:00 +0000 |
776 | +++ provider/skeleton/instance_firewall.go 2014-04-18 14:48:42 +0000 |
777 | @@ -0,0 +1,26 @@ |
778 | +// Copyright 2013 Canonical Ltd. |
779 | +// Licensed under the AGPLv3, see LICENCE file for details. |
780 | + |
781 | +package skeleton |
782 | + |
783 | +import ( |
784 | + "launchpad.net/juju-core/instance" |
785 | +) |
786 | + |
787 | +// Implementing the methods below (to do something other than return nil) will |
788 | +// cause `juju expose` to work when the firewall-mode is "instance". If you |
789 | +// implement one of them, you should implement them all. |
790 | + |
791 | +func (inst *environInstance) OpenPorts(machineId string, ports []instance.Port) error { |
792 | + logger.Warningf("pretending to open ports %v for instance %q", ports, inst.id) |
793 | + return nil |
794 | +} |
795 | + |
796 | +func (inst *environInstance) ClosePorts(machineId string, ports []instance.Port) error { |
797 | + logger.Warningf("pretending to close ports %v for instance %q", ports, inst.id) |
798 | + return nil |
799 | +} |
800 | + |
801 | +func (inst *environInstance) Ports(machineId string) ([]instance.Port, error) { |
802 | + return nil, nil |
803 | +} |
804 | |
805 | === added file 'provider/skeleton/provider.go' |
806 | --- provider/skeleton/provider.go 1970-01-01 00:00:00 +0000 |
807 | +++ provider/skeleton/provider.go 2014-04-18 14:48:42 +0000 |
808 | @@ -0,0 +1,121 @@ |
809 | +// Copyright 2013 Canonical Ltd. |
810 | +// Licensed under the AGPLv3, see LICENCE file for details. |
811 | + |
812 | +package skeleton |
813 | + |
814 | +import ( |
815 | + "errors" |
816 | + "fmt" |
817 | + |
818 | + "launchpad.net/loggo" |
819 | + |
820 | + "launchpad.net/juju-core/environs" |
821 | + "launchpad.net/juju-core/environs/config" |
822 | +) |
823 | + |
824 | +var logger = loggo.GetLogger("juju.provider.skeleton") |
825 | + |
826 | +type environProvider struct{} |
827 | + |
828 | +var providerInstance = environProvider{} |
829 | +var _ environs.EnvironProvider = providerInstance |
830 | + |
831 | +func init() { |
832 | + // This will only happen in binaries that actually import this provider |
833 | + // somewhere. To enable a provider, import it in the "providers/all" |
834 | + // package; please do *not* import individual providers anywhere else, |
835 | + // except in direct tests for that provider. |
836 | + environs.RegisterProvider("skeleton", providerInstance) |
837 | +} |
838 | + |
839 | +var errNotImplemented = errors.New("not implemented in skeleton provider") |
840 | + |
841 | +func (environProvider) Open(cfg *config.Config) (environs.Environ, error) { |
842 | + // You should probably not change this method; prefer to cause SetConfig |
843 | + // to completely configure an environment, regardless of the initial state. |
844 | + env := &environ{name: cfg.Name()} |
845 | + if err := env.SetConfig(cfg); err != nil { |
846 | + return nil, err |
847 | + } |
848 | + return env, nil |
849 | +} |
850 | + |
851 | +func (environProvider) Prepare(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { |
852 | + // You should probably not change this method; if you need to change how |
853 | + // configs are prepared, you should edit prepareConfig directly, lest the |
854 | + // code in this file drift gradually out of sync with that in config.go |
855 | + cfg, err := prepareConfig(cfg) |
856 | + if err != nil { |
857 | + return nil, err |
858 | + } |
859 | + return providerInstance.Open(cfg) |
860 | +} |
861 | + |
862 | +func (environProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) { |
863 | + // You should almost certainly not change this method; if you need to change |
864 | + // how configs are validated, you should edit validateConfig itself, to ensure |
865 | + // that your checks are always applied. |
866 | + newEcfg, err := validateConfig(cfg, nil) |
867 | + if err != nil { |
868 | + return nil, fmt.Errorf("invalid config: %v", err) |
869 | + } |
870 | + if old != nil { |
871 | + oldEcfg, err := validateConfig(old, nil) |
872 | + if err != nil { |
873 | + return nil, fmt.Errorf("invalid base config: %v", err) |
874 | + } |
875 | + if newEcfg, err = validateConfig(cfg, oldEcfg); err != nil { |
876 | + return nil, fmt.Errorf("invalid config change: %v", err) |
877 | + } |
878 | + } |
879 | + return newEcfg.Config, nil |
880 | +} |
881 | + |
882 | +func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { |
883 | + // If you keep configSecretFields up to date, this method should Just Work. |
884 | + ecfg, err := validateConfig(cfg, nil) |
885 | + if err != nil { |
886 | + return nil, err |
887 | + } |
888 | + secretAttrs := map[string]string{} |
889 | + for _, field := range configSecretFields { |
890 | + if value, ok := ecfg.attrs[field]; ok { |
891 | + if stringValue, ok := value.(string); ok { |
892 | + secretAttrs[field] = stringValue |
893 | + } else { |
894 | + // All your secret attributes must be strings at the moment. Sorry. |
895 | + // It's an expedient and hopefully temporary measure that helps us |
896 | + // plug a security hole in the API. |
897 | + return nil, fmt.Errorf( |
898 | + "secret %q field must have a string value; got %v", |
899 | + field, value, |
900 | + ) |
901 | + } |
902 | + } |
903 | + } |
904 | + return secretAttrs, nil |
905 | +} |
906 | + |
907 | +func (environProvider) BoilerplateConfig() string { |
908 | + // boilerplateConfig is kept in config.go, in the hope that people editing |
909 | + // config will keep it up to date. |
910 | + return boilerplateConfig |
911 | +} |
912 | + |
913 | +func (environProvider) PublicAddress() (string, error) { |
914 | + // Don't bother implementing this method until you're ready to deploy units. |
915 | + // You probably won't need to by that stage; it's due for retirement. If it |
916 | + // turns out that you do need to, remember that this method will *only* be |
917 | + // called in code running on an instance in an environment using this |
918 | + // provider; and it needs to return the address of *that* instance. |
919 | + return "", errNotImplemented |
920 | +} |
921 | + |
922 | +func (environProvider) PrivateAddress() (string, error) { |
923 | + // Don't bother implementing this method until you're ready to deploy units. |
924 | + // You probably won't need to by that stage; it's due for retirement. If it |
925 | + // turns out that you do need to, remember that this method will *only* be |
926 | + // called in code running on an instance in an environment using this |
927 | + // provider; and it needs to return the address of *that* instance. |
928 | + return "", errNotImplemented |
929 | +} |
930 | |
931 | === added file 'provider/skeleton/skeleton_test.go' |
932 | --- provider/skeleton/skeleton_test.go 1970-01-01 00:00:00 +0000 |
933 | +++ provider/skeleton/skeleton_test.go 2014-04-18 14:48:42 +0000 |
934 | @@ -0,0 +1,27 @@ |
935 | +// Copyright 2013 Canonical Ltd. |
936 | +// Licensed under the AGPLv3, see LICENCE file for details. |
937 | + |
938 | +package skeleton_test |
939 | + |
940 | +import ( |
941 | + "testing" |
942 | + |
943 | + gc "launchpad.net/gocheck" |
944 | + |
945 | + "launchpad.net/juju-core/environs" |
946 | + "launchpad.net/juju-core/provider/skeleton" |
947 | +) |
948 | + |
949 | +func TestPackage(t *testing.T) { |
950 | + gc.TestingT(t) |
951 | +} |
952 | + |
953 | +type SkeletonSuite struct{} |
954 | + |
955 | +var _ = gc.Suite(&SkeletonSuite{}) |
956 | + |
957 | +func (*SkeletonSuite) TestRegistered(c *gc.C) { |
958 | + provider, err := environs.Provider("skeleton") |
959 | + c.Assert(err, gc.IsNil) |
960 | + c.Assert(provider, gc.Equals, skeleton.Provider) |
961 | +} |
962 | |
963 | === added file 'provider/skeleton/storage.go' |
964 | --- provider/skeleton/storage.go 1970-01-01 00:00:00 +0000 |
965 | +++ provider/skeleton/storage.go 2014-04-18 14:48:42 +0000 |
966 | @@ -0,0 +1,53 @@ |
967 | +// Copyright 2013 Canonical Ltd. |
968 | +// Licensed under the AGPLv3, see LICENCE file for details. |
969 | + |
970 | +package skeleton |
971 | + |
972 | +import ( |
973 | + "io" |
974 | + |
975 | + "launchpad.net/juju-core/environs/storage" |
976 | + "launchpad.net/juju-core/utils" |
977 | +) |
978 | + |
979 | +type environStorage struct { |
980 | + ecfg *environConfig |
981 | +} |
982 | + |
983 | +var _ storage.Storage = (*environStorage)(nil) |
984 | + |
985 | +func newStorage(ecfg *environConfig) (storage.Storage, error) { |
986 | + return &environStorage{ecfg}, nil |
987 | +} |
988 | + |
989 | +func (s *environStorage) List(prefix string) ([]string, error) { |
990 | + return nil, errNotImplemented |
991 | +} |
992 | + |
993 | +func (s *environStorage) URL(name string) (string, error) { |
994 | + return "", errNotImplemented |
995 | +} |
996 | + |
997 | +func (s *environStorage) Get(name string) (io.ReadCloser, error) { |
998 | + return nil, errNotImplemented |
999 | +} |
1000 | + |
1001 | +func (s *environStorage) Put(name string, r io.Reader, length int64) error { |
1002 | + return errNotImplemented |
1003 | +} |
1004 | + |
1005 | +func (s *environStorage) Remove(name string) error { |
1006 | + return errNotImplemented |
1007 | +} |
1008 | + |
1009 | +func (s *environStorage) RemoveAll() error { |
1010 | + return errNotImplemented |
1011 | +} |
1012 | + |
1013 | +func (s *environStorage) DefaultConsistencyStrategy() utils.AttemptStrategy { |
1014 | + return utils.AttemptStrategy{} |
1015 | +} |
1016 | + |
1017 | +func (s *environStorage) ShouldRetry(err error) bool { |
1018 | + return false |
1019 | +} |
Reviewers: mp+189638_ code.launchpad. net,
Message:
Please take a look.
Description:
skeleton provider
...and rudimentary initial implementation notes.
https:/ /code.launchpad .net/~fwereade/ juju-core/ provider- skeleton/ +merge/ 189638
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/14494043/
Affected files (+926, -0 lines): providers. txt skeleton/ config. go skeleton/ config_ test.go skeleton/ environ. go skeleton/ environ_ firewall. go skeleton/ environ_ instance. go skeleton/ export_ test.go skeleton/ instance. go skeleton/ instance_ firewall. go skeleton/ provider. go skeleton/ skeleton_ test.go skeleton/ storage. go
A [revision details]
A doc/hacking-
A provider/
A provider/
A provider/
A provider/
A provider/
A provider/
A provider/
A provider/
A provider/
A provider/
A provider/