Merge lp:~hazmat/juju-jitsu/import-export into lp:juju-jitsu

Proposed by Kapil Thangavelu
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
Reviewer Review Type Date Requested Status
Juan L. Negron (community) Approve
Review via email: mp+112819@code.launchpad.net

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

https://codereview.appspot.com/6350057/

To post a comment you must log in.
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

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

Revision history for this message
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

review: Approve
lp:~hazmat/juju-jitsu/import-export updated
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

Revision history for this message
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

Revision history for this message
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://codereview.appspot.com/6350057

https://codereview.appspot.com/6350057/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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()

Subscribers

People subscribed via source and target branches