Merge lp:~hazmat/juju-jitsu/import-export into lp:juju-jitsu
- import-export
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 62 |
Proposed branch: | lp:~hazmat/juju-jitsu/import-export |
Merge into: | lp:juju-jitsu |
Diff against target: |
419 lines (+410/-0) 2 files modified
sub-commands/export (+169/-0) sub-commands/import (+241/-0) |
To merge this branch: | bzr merge lp:~hazmat/juju-jitsu/import-export |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juan L. Negron (community) | Approve | ||
Review via email: mp+112819@code.launchpad.net |
Commit message
Description of the change
jitsu environment import export
Deployment automation for juju. We see lots of shell scripts for creating
environments. Ideally we could just edit a json file or dump an existing
environment to create a new one. These pair of commands (export and import) do
just that.
you can dump an environment to json with
jitsu export -e env_name > env.json
it will capture services, unit counts, constraints, service config, relations, etc.
That can in turn be imported to another environment with
jitsu import -e env_name env.json
R=negronil
Kapil Thangavelu (hazmat) wrote : | # |
Juan L. Negron (negronjl) wrote : | # |
Kapil:
Thanks for this. It is very useful.
Some thoughts about both export and import:
- They could both use more verbose help that details how to use the tools.
Some thoughts about import:
- It appears that import needs a local repository ( or I don't know how to tell it to use cs: which could be alleviated by more verbose help )
I tried both export and import. They both work.
Thanks Kapil.
Thanks,
Juan
- 63. By Kapil Thangavelu
-
import allows stdin/pipes, doesn't require local repo when using store exclusively, distinguish file not present vs json errors, doc strings for both import&export
Kapil Thangavelu (hazmat) wrote : | # |
On Fri, Jun 29, 2012 at 3:06 PM, Juan L. Negron <email address hidden>wrote:
> Review: Approve
>
> Kapil:
>
> Thanks for this. It is very useful.
>
> Some thoughts about both export and import:
> - They could both use more verbose help that details how to use the tools.
>
>
Added more verbose docs
> Some thoughts about import:
> - It appears that import needs a local repository ( or I don't know how to
> tell it to use cs: which could be alleviated by more verbose help )
>
that was an oversight (i had a JUJU_REPOSITORY set during dev), fixed. also
add ability to pipe directly into jitsu import from export.
cheers,
Kapil
Kapil Thangavelu (hazmat) wrote : | # |
*** Submitted:
jitsu environment import export
Deployment automation for juju. We see lots of shell scripts for
creating
environments. Ideally we could just edit a json file or dump an existing
environment to create a new one. These pair of commands (export and
import) do
just that.
you can dump an environment to json with
jitsu export -e env_name > env.json
it will capture services, unit counts, constraints, service config,
relations, etc.
That can in turn be imported to another environment with
jitsu import -e env_name env.json
R=negronil
R=negronjl
CC=
https:/
Preview Diff
1 | === added file 'sub-commands/export' |
2 | --- sub-commands/export 1970-01-01 00:00:00 +0000 |
3 | +++ sub-commands/export 2012-06-30 15:56:19 +0000 |
4 | @@ -0,0 +1,169 @@ |
5 | +#!/usr/bin/env python |
6 | + |
7 | +import argparse |
8 | +import json |
9 | +import logging |
10 | +import os |
11 | +import zookeeper |
12 | + |
13 | +from twisted.internet.defer import inlineCallbacks |
14 | +from twisted.internet import reactor |
15 | + |
16 | +from juju.environment.config import EnvironmentsConfig |
17 | + |
18 | +from juju.state.service import ServiceStateManager |
19 | +from juju.state.relation import RelationStateManager |
20 | +from juju.state.environment import EnvironmentStateManager |
21 | + |
22 | + |
23 | +log = logging.getLogger("jitsu.export") |
24 | + |
25 | + |
26 | +@inlineCallbacks |
27 | +def export(env): |
28 | + |
29 | + provider = env.get_machine_provider() |
30 | + client = yield provider.connect() |
31 | + |
32 | + data = {'services': [], |
33 | + 'relations': []} |
34 | + |
35 | + rels = set() |
36 | + |
37 | + # Get services |
38 | + services = ServiceStateManager(client) |
39 | + relations = RelationStateManager(client) |
40 | + environment = EnvironmentStateManager(client) |
41 | + |
42 | + for s in (yield services.get_all_service_states()): |
43 | + units = yield s.get_unit_names() |
44 | + charm_id = yield s.get_charm_id() |
45 | + constraints = yield s.get_constraints() |
46 | + config = yield s.get_config() |
47 | + exposed = yield s.get_exposed_flag() |
48 | + |
49 | + log.info("processing service %s / %s", s.service_name, charm_id) |
50 | + |
51 | + # Really!? relation-type not available via state api |
52 | + topology = yield relations._read_topology() |
53 | + |
54 | + # More efficient to do this via topology, ie op per rel |
55 | + # instead inspection of both sides by each endpoint, which is |
56 | + # kinda of gross, need some state api support. But trying to |
57 | + # stick to the state api as much as possible. |
58 | + svc_rels = yield relations.get_relations_for_service(s) |
59 | + |
60 | + |
61 | + for r in svc_rels: |
62 | + relation_type = topology.get_relation_type(r.internal_relation_id) |
63 | + rdata = [ |
64 | + (s.service_name, |
65 | + relation_type, |
66 | + r.relation_name, |
67 | + r.relation_role, |
68 | + r.relation_scope)] |
69 | + if r.relation_role == "peer": |
70 | + rels.add(tuple(rdata)) |
71 | + continue |
72 | + rel_services = yield r.get_service_states() |
73 | + |
74 | + # Get the endpoint's svc rel state. |
75 | + found = None |
76 | + for ep_svc in rel_services: |
77 | + if ep_svc.service_name == s.service_name: |
78 | + continue |
79 | + ep_rels = yield relations.get_relations_for_service(ep_svc) |
80 | + for ep_r in ep_rels: |
81 | + if ep_r.internal_relation_id != r.internal_relation_id: |
82 | + continue |
83 | + found = (ep_svc, ep_r) |
84 | + break |
85 | + |
86 | + if not found: |
87 | + log.info("Couldn't decipher rel %s %s", |
88 | + s.service_name, r) |
89 | + continue |
90 | + |
91 | + ep_svc, ep_r = found |
92 | + rdata.append( |
93 | + (ep_svc.service_name, |
94 | + relation_type, |
95 | + ep_r.relation_name, |
96 | + ep_r.relation_role, |
97 | + ep_r.relation_scope)) |
98 | + rdata.sort() |
99 | + rels.add(tuple(rdata)) |
100 | + |
101 | + data['services'].append( |
102 | + {'name': s.service_name, |
103 | + 'charm': charm_id, |
104 | + 'exposed': exposed, |
105 | + 'unit_count': len(units), |
106 | + 'constraints': constraints.data, |
107 | + 'config': dict(config.items())}) |
108 | + |
109 | + data['relations'] = list(rels) |
110 | + data['constraints'] = (yield environment.get_constraints()).data |
111 | + print json.dumps(data, indent=2) |
112 | + |
113 | + |
114 | +@inlineCallbacks |
115 | +def run_one(func, *args, **kw): |
116 | + try: |
117 | + yield func(*args, **kw) |
118 | + finally: |
119 | + reactor.stop() |
120 | + |
121 | + |
122 | +def get_environment(env_option): |
123 | + env_config = EnvironmentsConfig() |
124 | + env_config.load_or_write_sample() |
125 | + |
126 | + if env_option is None: |
127 | + environment = env_config.get_default() |
128 | + else: |
129 | + environment = env_config.get(env_option) |
130 | + |
131 | + return env_config, environment |
132 | + |
133 | + |
134 | +def get_parser(): |
135 | + parser = argparse.ArgumentParser( |
136 | + description="Export an environment") |
137 | + |
138 | + parser.add_argument( |
139 | + "--environment", "-e", default=os.environ.get("JUJU_ENV"), |
140 | + help="Environment to operate on.") |
141 | + |
142 | + return parser |
143 | + |
144 | + |
145 | +def main(): |
146 | + """ |
147 | + Export a juju environment. |
148 | + |
149 | + Prints a json formatted file containing information needed to recreate |
150 | + an environment (services, relations, config). |
151 | + |
152 | + Example: |
153 | + jitsu export -e env_name |
154 | + |
155 | + Also respects JUJU_ENV for environment to use, defaults to default env. |
156 | + """ |
157 | + parser = get_parser() |
158 | + options = parser.parse_args() |
159 | + |
160 | + log_options = { |
161 | + "level": logging.DEBUG, |
162 | + "format": "%(asctime)s %(name)s:%(levelname)s %(message)s"} |
163 | + logging.basicConfig(**log_options) |
164 | + |
165 | + zookeeper.set_debug_level(0) |
166 | + |
167 | + env_config, environment = get_environment(options.environment) |
168 | + reactor.callWhenRunning(run_one, export, environment) |
169 | + reactor.run() |
170 | + |
171 | + |
172 | +if __name__ == '__main__': |
173 | + main() |
174 | |
175 | === added file 'sub-commands/import' |
176 | --- sub-commands/import 1970-01-01 00:00:00 +0000 |
177 | +++ sub-commands/import 2012-06-30 15:56:19 +0000 |
178 | @@ -0,0 +1,241 @@ |
179 | +#!/usr/bin/env python |
180 | + |
181 | +import argparse |
182 | +import json |
183 | +import logging |
184 | +import os |
185 | +import sys |
186 | +import textwrap |
187 | +import zookeeper |
188 | + |
189 | +from txzookeeper.retry import RetryClient |
190 | + |
191 | +from twisted.internet.defer import inlineCallbacks |
192 | +from twisted.internet import reactor |
193 | + |
194 | +from juju.charm.errors import CharmNotFound |
195 | +from juju.charm.publisher import CharmPublisher |
196 | +from juju.charm.repository import ( |
197 | + CharmURL, LocalCharmRepository, RemoteCharmRepository, CS_STORE_URL) |
198 | +from juju.control.utils import sync_environment_state |
199 | +from juju.environment.config import EnvironmentsConfig |
200 | +from juju.errors import JujuError |
201 | + |
202 | +from juju.state.endpoint import RelationEndpoint |
203 | +from juju.state.service import ServiceStateManager |
204 | +from juju.state.machine import MachineStateManager |
205 | +from juju.state.placement import _unassigned_placement |
206 | +from juju.state.relation import RelationStateManager |
207 | + |
208 | + |
209 | + |
210 | +log = logging.getLogger("jitsu.import") |
211 | + |
212 | + |
213 | +@inlineCallbacks |
214 | +def load(env_configs, env, repo_dir, data): |
215 | + """ |
216 | + |
217 | + Check for name conflicts |
218 | + - Upload charms |
219 | + - Create services (+ config, constraints) |
220 | + - Add units |
221 | + - Add relations |
222 | + """ |
223 | + provider = env.get_machine_provider() |
224 | + client = yield provider.connect() |
225 | + client = RetryClient(client) |
226 | + |
227 | + # Make sure the provider is ready to go. |
228 | + yield sync_environment_state(client, env_configs, env.name) |
229 | + |
230 | + # Get state managers |
231 | + services = ServiceStateManager(client) |
232 | + relations = RelationStateManager(client) |
233 | + machines = MachineStateManager(client) |
234 | + |
235 | + # First detect conflicts |
236 | + existing_services = yield services.get_all_service_states() |
237 | + env_svc_names = [e.service_name for e in existing_services] |
238 | + import_svc_names = [i['name'] for i in data['services']] |
239 | + conflict_svc_names = set(env_svc_names) & set(import_svc_names) |
240 | + if conflict_svc_names: |
241 | + raise JujuError( |
242 | + "Import has name conflicts with existing services %s" % ( |
243 | + ", ".join(conflict_svc_names))) |
244 | + |
245 | + # Next find all the nesc charms |
246 | + if repo_dir: |
247 | + repository = LocalCharmRepository(repo_dir) |
248 | + else: |
249 | + repository = None |
250 | + |
251 | + store = RemoteCharmRepository(CS_STORE_URL) |
252 | + |
253 | + charms = [] |
254 | + for s in data['services']: |
255 | + curl = CharmURL.infer(s['charm'], env.default_series) |
256 | + if s['charm'].startswith('local:'): |
257 | + if repository is None: |
258 | + raise JujuError("Local charm needed but repository not specified") |
259 | + try: |
260 | + charm = yield repository.find(curl) |
261 | + except CharmNotFound: |
262 | + # retry with less specific version |
263 | + charm = yield repository.find( |
264 | + CharmURL.infer( |
265 | + s['charm'][:s['charm'].rfind('-')], |
266 | + env.default_series)) |
267 | + charms.append((s['charm'], charm)) |
268 | + else: |
269 | + assert s['charm'].startswith('cs:') |
270 | + charm = yield store.find(curl) |
271 | + charms.append((s['charm'], charm)) |
272 | + |
273 | + # Upload charms to the environment |
274 | + publisher = CharmPublisher(client, provider.get_file_storage()) |
275 | + for cid, c in charms: |
276 | + log.info("Publishing charm %s", cid) |
277 | + yield publisher.add_charm( |
278 | + str(CharmURL.infer(cid, env.default_series)), |
279 | + c) |
280 | + charm_states = yield publisher.publish() |
281 | + |
282 | + # Index by url |
283 | + charm_map = dict([(ci[0], cs) for ci, cs in zip(charms, charm_states)]) |
284 | + constraint_set = yield provider.get_constraint_set() |
285 | + |
286 | + # Create services |
287 | + for s in data['services']: |
288 | + log.info("Creating service %s %s", s['name'], charm_map[s['charm']].id) |
289 | + constraints = constraint_set.load(s['constraints']) |
290 | + svc = yield services.add_service_state( |
291 | + s['name'], |
292 | + charm_map[s['charm']], |
293 | + constraints) |
294 | + config = yield svc.get_config() |
295 | + config.update(s['config']) |
296 | + yield config.write() |
297 | + |
298 | + # Create units |
299 | + log.info("Creating units") |
300 | + for i in range(s['unit_count']): |
301 | + unit = yield svc.add_unit_state() |
302 | + yield _unassigned_placement(client, machines, unit) |
303 | + |
304 | + # Add relations |
305 | + for r in data['relations']: |
306 | + if len(r) == 1: |
307 | + eps = [RelationEndpoint(*r[0])] |
308 | + else: |
309 | + eps = [RelationEndpoint(*r[0]), RelationEndpoint(*r[1])] |
310 | + log.info("Adding relation %s" %( |
311 | + " ".join(map( |
312 | + lambda r: "%s:%s %s" % (r.service_name, r.relation_name, r.relation_role), |
313 | + eps)))) |
314 | + yield relations.add_relation_state(*eps) |
315 | + |
316 | + |
317 | + |
318 | +def get_parser(): |
319 | + parser = argparse.ArgumentParser( |
320 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
321 | + description=textwrap.dedent(main.__doc__)) |
322 | + |
323 | + parser.add_argument( |
324 | + "--environment", "-e", default=os.environ.get("JUJU_ENV"), |
325 | + help="Environment to operate on.") |
326 | + |
327 | + parser.add_argument( |
328 | + "--repository", "-r", default=os.environ.get("JUJU_REPOSITORY"), |
329 | + help="Reposiory for local charms.") |
330 | + |
331 | + parser.add_argument( |
332 | + "export_file", nargs=None, |
333 | + help="Environment export file") |
334 | + |
335 | + return parser |
336 | + |
337 | + |
338 | +@inlineCallbacks |
339 | +def run_one(func, *args, **kw): |
340 | + try: |
341 | + yield func(*args, **kw) |
342 | + finally: |
343 | + reactor.stop() |
344 | + |
345 | + |
346 | +def get_environment(env_option): |
347 | + env_config = EnvironmentsConfig() |
348 | + env_config.load_or_write_sample() |
349 | + |
350 | + if env_option is None: |
351 | + environment = env_config.get_default() |
352 | + else: |
353 | + environment = env_config.get(env_option) |
354 | + |
355 | + return env_config, environment |
356 | + |
357 | + |
358 | + |
359 | +def main(): |
360 | + """ |
361 | + Load an environment export into an environment |
362 | + |
363 | + jitsu import <environment-export-json-file> |
364 | + |
365 | + The target environment must already be bootstrapped. Any conflicts |
366 | + among service names between the export and the target environment |
367 | + are fatal (and detected early). |
368 | + |
369 | + if the environment uses local charms then a local repository needs |
370 | + to be specified. For eample |
371 | + |
372 | + jitsu import --repository=$HOME/charms env.json |
373 | + |
374 | + Both JUJU_REPOSITORY and JUJU_ENV variables are respected. |
375 | + |
376 | + Load also supports reading from stdin ie. |
377 | + |
378 | + jitsu export -e staging | jitsu import -e production |
379 | + """ |
380 | + parser = get_parser() |
381 | + options = parser.parse_args() |
382 | + |
383 | + log_options = { |
384 | + "level": logging.DEBUG, |
385 | + "format": "%(asctime)s %(name)s:%(levelname)s %(message)s"} |
386 | + logging.basicConfig(**log_options) |
387 | + |
388 | + zookeeper.set_debug_level(0) |
389 | + |
390 | + if options.export_file == "-": |
391 | + export_file = sys.stdin |
392 | + else: |
393 | + export_path = os.path.abspath( |
394 | + os.path.expanduser(options.export_file)) |
395 | + if not os.path.exists(export_path): |
396 | + raise JujuError("Invalid export file, does not exist %s" % export_path) |
397 | + export_file = open(export_path) |
398 | + |
399 | + try: |
400 | + export_data = json.loads(export_file.read()) |
401 | + except Exception, e: |
402 | + raise JujuError( |
403 | + "Invalid environment export, malformed json %s %s" % ( |
404 | + options.export_file, e)) |
405 | + |
406 | + env_config, environment = get_environment(options.environment) |
407 | + |
408 | + reactor.callWhenRunning( |
409 | + run_one, load, |
410 | + env_config, |
411 | + environment, |
412 | + options.repository, |
413 | + export_data, |
414 | + ) |
415 | + reactor.run() |
416 | + |
417 | + |
418 | +if __name__ == '__main__': |
419 | + main() |
Reviewers: mp+112819_ code.launchpad. net,
Message:
Please take a look.
Description:
jitsu environment import export
Deployment automation for juju. We see lots of shell scripts for
creating
environments. Ideally we could just edit a json file or dump an existing
environment to create a new one. These pair of commands (export and
import) do
just that.
you can dump an environment to json with
jitsu export -e env_name > env.json
it will capture services, unit counts, constraints, service config,
relations, etc.
That can in turn be imported to another environment with
jitsu import -e env_name env.json
https:/ /code.launchpad .net/~hazmat/ juju-jitsu/ import- export/ +merge/ 112819
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/6350057/
Affected files:
A [revision details]
A sub-commands/export
A sub-commands/import