Merge bootstack-ops:juju-bundle-export-fixes into bootstack-ops:master

Proposed by Alvaro Uria
Status: Merged
Approved by: Brad Marshall
Approved revision: d24fe761129e9a082019a51a8a91168d0fa5abff
Merge reported by: Alvaro Uria
Merged at revision: d24fe761129e9a082019a51a8a91168d0fa5abff
Proposed branch: bootstack-ops:juju-bundle-export-fixes
Merge into: bootstack-ops:master
Diff against target: 286 lines (+121/-30)
1 file modified
ops-bundle/scripts/juju_bundle_export.py (+121/-30)
Reviewer Review Type Date Requested Status
Brad Marshall (community) Approve
Review via email: mp+327232@code.launchpad.net
To post a comment you must log in.
fa8a3a6... by Alvaro Uria

juju_bundle_export.py exclude services (--exclude)

 * Add [--exclude APP_NAME] to filter the list of services and relations
   --exclude can be repeated multiple times
 * Add docstrings for a couple more internal methods (to gather charms
 interface for relation disambiguation
 * Make --fullstatus mutually exclusive with --charm or --exclude
 parameters.

Revision history for this message
Alvaro Uria (aluria) wrote :

I've also fixed DEPLOY_NAME default to "juju switch" output instead of "dummy-stage0" (which could be passed via "--name dummy-stage0").

20a3ff3... by Alvaro Uria

minor fix on parser.print_help()

Revision history for this message
Brad Marshall (brad-marshall) wrote :

This looks good, much better way of doing the exclude parameter than I had done. As per my comment, why is fullstatus mutually exclusive with exclude? I need both for prometheus deployments.

review: Needs Fixing
Revision history for this message
Brad Marshall (brad-marshall) wrote :

I can confirm that by removing the mutually exclusive check for fullstatus and exclude I can do the prometheus deployment.

d5e77cb... by Alvaro Uria

Fix: --fullstatus is only mutually exclusive with --charm

d24fe76... by Alvaro Uria

minor fix: remove comment about --exclude/--fullstatus being mutually exclusive

Revision history for this message
Brad Marshall (brad-marshall) wrote :

Looks good to me, +1

review: Approve
Revision history for this message
Alvaro Uria (aluria) wrote :

changes squashed into master

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/ops-bundle/scripts/juju_bundle_export.py b/ops-bundle/scripts/juju_bundle_export.py
2index 8256cdf..9d3f27c 100755
3--- a/ops-bundle/scripts/juju_bundle_export.py
4+++ b/ops-bundle/scripts/juju_bundle_export.py
5@@ -2,6 +2,7 @@
6 # Standalone script to export running environment as a juju-deployer bundle
7 #
8 # Author: JuanJo Ciarlante <jjo@canonical.com>
9+# Modified: Alvaro Uria <alvaro.uria@canonical.com>
10 # License: GPLv3+
11 #
12 # Eg usage:
13@@ -37,6 +38,10 @@ CHARM_DIR_MAP = {
14
15
16 def charmstore_get(series, charm):
17+ """charmstore_get(str, str) -> str
18+
19+ ie. charmstore_get('xenial', 'ubuntu') -> 'cs:xenial/ubuntu-10'
20+ """
21 cs_reply = requests.get('https://api.jujucharms.com/v4/search', params={
22 'name': charm,
23 'series': series,
24@@ -64,6 +69,12 @@ class Bundle(object):
25 # as juju2 doesn't have a relations dict, need to build it from
26 # each service iteration
27 def _get_rel_name(self, src, tgt):
28+ """_get_rel_name(str, str) -> str
29+
30+ ['ceph:client', 'nova-compute:ceph'],
31+ src == 'nova-compute' and tgt == 'ceph'
32+ r == 'client'
33+ """
34 if not self.env_status:
35 logger.warning("_get_rel_name called without initialized env_status")
36 return None
37@@ -74,6 +85,11 @@ class Bundle(object):
38 return None
39
40 def _load_rels(self, svc_name):
41+ """_loads_rel(str) -> set((str, str), (str, str)...)
42+
43+ ['ceph:client', 'nova-compute:ceph'],
44+ svc_name == 'nova-compute' -> r_name == 'ceph'
45+ """
46 rels = set()
47 svc_rels = self.env_status['services'][svc_name].get(
48 'relations', {})
49@@ -85,6 +101,9 @@ class Bundle(object):
50 # Skip peer relations
51 if r_svc == svc_name:
52 continue
53+ # if charm is excluded, skip it
54+ elif self.args.exclude and r_svc in self.args.exclude:
55+ continue
56 rr_name = self._get_rel_name(svc_name, r_svc)
57 rels.add(
58 tuple(sorted([
59@@ -93,45 +112,85 @@ class Bundle(object):
60 return rels
61
62 def export(self):
63+ """Parses Juju FullStatus from apiserver (json format).
64+ Stores juju-deployer services/relations into self.deploy
65+
66+ juju_version: 1
67+ https://github.com/juju/juju/blob/1.25/apiserver/params/status.go#L64
68+ juju_version: 2
69+ https://github.com/juju/juju/blob/2.2/apiserver/params/status.go#L107
70+ """
71 self.env_status = self.env.status()
72 services = {}
73 relations = set()
74- subordto_key = [None, 'SubordinateTo', 'subordinate-to'][self.env.juju_version]
75- charm_key = [None, 'Charm', 'charm'][self.env.juju_version]
76+ subordto_key = {1: 'SubordinateTo'}.get(self.env.juju_version,
77+ 'subordinate-to')
78+ charm_key = {1: 'Charm'}.get(self.env.juju_version,
79+ 'charm')
80 # print( yaml.dump(self.env_status.get('services'), default_flow_style=False))
81 for service_name, service_val in self.env_status.get('services', {}).items():
82+ # if charm is excluded, skip it
83+ if self.args.exclude and service_name in self.args.exclude:
84+ continue
85+
86 units = service_val.get('units', None)
87 subord_to = service_val.get(subordto_key)
88+
89+ # skip subordinate charms for fake stage0 (minimal info)
90+ if subord_to and not self.args.fullstatus:
91+ continue
92+
93 # Subordinate units show no Units
94 if units or subord_to:
95 charm = '-'.join(service_val[charm_key].split('-')[0:-1])
96+
97+ # if charm name specified, do not process unmatched ones
98+ if self.args.charm_name and \
99+ self.args.charm_name not in charm:
100+ continue
101+
102 service = {
103 'charm': charm
104 }
105 if units:
106 service['num_units'] = len(units)
107- if not self.args.minimal:
108+
109+ # List all service/application options
110+ # not needed when generating a fake stage0 (minimal)
111+ # needed to redeploy the environment
112+ if self.args.fullstatus:
113 config = self.env.get_config(service_name)
114 options = {k: v['value'] for k, v in config.items()
115 if not v.get('default', False)}
116 if options:
117 service['options'] = options
118+ relations.update(self._load_rels(service_name))
119+
120 services[service_name] = service
121- relations.update(self._load_rels(service_name))
122- relations = [list(rel) for rel in sorted(relations)]
123
124- self.deploy = {
125- 'services': services,
126- 'relations': relations,
127- }
128+ if self.args.fullstatus:
129+ relations = [list(rel) for rel in sorted(relations)]
130+ self.deploy = {'services': services,
131+ 'relations': relations}
132+ else:
133+ self.deploy = {'services': services}
134+
135 self.resolve_charm_local_urls()
136 return (self.env_name, self.deploy, self.codetree)
137
138 def init_local_files(self):
139+ """Stores codetree file into self.local_charm_url_map dict
140+ Initializes self.local_charms_dir
141+ """
142 self.local_charm_url_map = {}
143 bootstack_dir = self.args.bootstackdir.format(env_name=self.env_name)
144+
145+ # XXX(aluria) Support mojo
146+ # collect_file = 'collect'
147+ collect_file = 'customer/configs/bootstack-charms.bzr'
148 bs_codetree_file = os.path.join(bootstack_dir,
149- "customer/configs/bootstack-charms.bzr")
150+ collect_file)
151+
152 if (os.path.exists(bs_codetree_file)):
153 with open(bs_codetree_file) as bs_ct_f:
154 for line in bs_ct_f:
155@@ -141,23 +200,32 @@ class Bundle(object):
156 if (len(key_val) != 2):
157 continue
158 self.local_charm_url_map.update({key_val[0]: key_val[1]})
159- self.local_charms_dir = None
160- local_dir = os.path.join(bootstack_dir, "charms")
161+
162+ local_dir = os.path.join(bootstack_dir, 'charms')
163 if os.path.exists(local_dir):
164 self.local_charms_dir = local_dir
165+ else:
166+ self.local_charms_dir = None
167
168 def local_charm_dir(self, charm_dir):
169- if self.local_charms_dir and os.path.exists(
170- os.path.join(self.local_charms_dir, charm_dir)):
171+ """Returns path to local charm
172+ """
173+ if self.local_charms_dir and \
174+ os.path.exists(os.path.join(self.local_charms_dir,
175+ charm_dir)):
176 return os.path.join(self.local_charms_dir, charm_dir)
177 return None
178
179 def resolve_charm_local_urls(self):
180+ """Parses codetree file
181+ Rewrites charm reference if not found locally
182+ """
183 self.init_local_files()
184 for service, service_val in self.deploy['services'].items():
185 charm_url = service_val['charm']
186 if charm_url.startswith('local:'):
187- service_val['charm'] = self.get_charm_local_url(service, charm_url)
188+ service_val['charm'] = self.get_charm_local_url(service,
189+ charm_url)
190
191 def get_charm_local_url(self, service, charm_url):
192 """Return a line for codetree, in the form of
193@@ -184,11 +252,13 @@ class Bundle(object):
194 charm, charm_dir).format(series=series)
195
196 # Try to resolve local charm in this order:
197- for func, args in (
198- (self.local_charm_dir, [charm_dir]),
199- (self.local_charm_url_map.get, [charm_dir]),
200- (charmstore_get, [series, charm]),
201- ):
202+ # 1) charm found on local charms folder
203+ # 2) def found on codetree file
204+ # 3) download from the charmstore
205+ for func, args in ((self.local_charm_dir, [charm_dir]),
206+ (self.local_charm_url_map.get, [charm_dir]),
207+ (charmstore_get, [series, charm]),
208+ ):
209 charm_url_new = func(*args)
210 # If we're overriding the charm_url to point locally::
211 # - point bundle to local charm dir
212@@ -200,7 +270,11 @@ class Bundle(object):
213 return charm_url
214
215 def get_named_bundle(self):
216- deploy_name = self.args.deploy_name if self.args.deploy_name else self.env_name
217+ if self.args.deploy_name:
218+ deploy_name = self.args.deploy_name
219+ else:
220+ deploy_name = self.env_name
221+
222 named_bundle = ({deploy_name: self.deploy})
223 return yaml.dump(named_bundle, default_flow_style=False)
224
225@@ -216,35 +290,52 @@ def parse_args():
226 parser = argparse.ArgumentParser(
227 description='Export juju environment as a bundle')
228 parser.add_argument('env_name', nargs='?',
229- help='Environment name (if not default')
230- parser.add_argument('-o', dest='outfile', nargs='?',
231- type=argparse.FileType('w'), default=sys.stdout)
232- parser.add_argument('-c', dest='codetreefile', nargs='?',
233- type=argparse.FileType('w'), default=None)
234+ help='Environment name (if not default)')
235 parser.add_argument('-b', dest='bootstackdir', nargs='?',
236 help='Local charms directory',
237 default='/home/jujumanage/{env_name}')
238+ parser.add_argument('-c', dest='codetreefile', nargs='?',
239+ type=argparse.FileType('w'), default=None)
240+ parser.add_argument('-o', dest='outfile', nargs='?',
241+ type=argparse.FileType('w'), default=sys.stdout)
242 parser.add_argument('--name', dest='deploy_name', nargs='?',
243 help='Deployment name to save, instead of <JUJU_ENV>')
244- parser.add_argument('--minimal', dest='minimal', action='store_true',
245- help='Do not include service "options"')
246 parser.add_argument('--no-hacks', dest='no_hacks', action='store_true',
247 help='Do not apply bootstack naming hacks')
248 parser.add_argument('--debug', action='store_true', default=False,
249 help='show debugging')
250- return parser.parse_args(sys.argv[1:])
251+ parser.add_argument('--fullstatus', dest='fullstatus', action='store_true',
252+ help='Includes service "options", and relations')
253+ parser.add_argument('--charm', dest='charm_name', nargs='?',
254+ help='Filter services by charm name used')
255+ parser.add_argument('--exclude', action='append', default=[],
256+ help='Exclude services by their name')
257+
258+ # XXX(aluria) if "--charm ubuntu --fullstatus",
259+ # services listed are only the ones using the "ubuntu" charm
260+ # relations listed could be other than the ones using the
261+ # "ubuntu" charm
262+ result = parser.parse_args(sys.argv[1:])
263+ if result.fullstatus and result.charm_name:
264+ print('Error: --fullstatus is mutually exclusive with '
265+ '--charm parameter.\n')
266+ parser.print_help()
267+ sys.exit(1)
268+ return result
269
270
271 def main():
272 args = parse_args()
273+
274 if args.debug:
275 logger.setLevel(logging.DEBUG)
276+
277 bundle = Bundle(args)
278 bundle.export()
279 print(bundle.get_named_bundle(), file=args.outfile)
280+
281 if args.codetreefile:
282 print(bundle.get_codetree(), file=args.codetreefile)
283
284-
285 if __name__ == '__main__':
286 main()

Subscribers

People subscribed via source and target branches

to all changes: