Merge bootstack-ops:juju-bundle-export-fixes into bootstack-ops:master
- Git
- lp:bootstack-ops
- juju-bundle-export-fixes
- Merge into master
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Marshall (community) | Approve | ||
Review via email: mp+327232@code.launchpad.net |
Commit message
Description of the change
- 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.
Alvaro Uria (aluria) wrote : | # |
- 20a3ff3... by Alvaro Uria
-
minor fix on parser.print_help()
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.
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
Brad Marshall (brad-marshall) wrote : | # |
Looks good to me, +1
Alvaro Uria (aluria) wrote : | # |
changes squashed into master
Preview Diff
1 | diff --git a/ops-bundle/scripts/juju_bundle_export.py b/ops-bundle/scripts/juju_bundle_export.py |
2 | index 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() |
I've also fixed DEPLOY_NAME default to "juju switch" output instead of "dummy-stage0" (which could be passed via "--name dummy-stage0").