Merge bootstack-ops:juju-select-largest-model-improve into bootstack-ops:master

Proposed by Giuseppe Petralia
Status: Rejected
Rejected by: Haw Loeung
Proposed branch: bootstack-ops:juju-select-largest-model-improve
Merge into: bootstack-ops:master
Diff against target: 393 lines (+124/-142)
1 file modified
bootstack-ops/cloud_report.py (+124/-142)
Reviewer Review Type Date Requested Status
Andrea Ieri (community) Needs Fixing
Review via email: mp+373037@code.launchpad.net

Commit message

Accept comments from juju-select-largest-model branch

https://code.launchpad.net/~canonical-bootstack/bootstack-ops/+git/bootstack-ops/+merge/366113

- Unify retry mechanism for juju and maas calls
- Use --format=yaml for any juju call

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Giuseppe Petralia (peppepetra) wrote :

Cloud_report generation has been tested in: Kambi, Docapost and Plus65

Revision history for this message
Andrea Ieri (aieri) wrote :

Some cleanups inline.
Also: there's some trailing whitespace that could be removed.

review: Needs Fixing

Unmerged commits

30c266e... by Giuseppe Petralia

Accept comments from juju-select-largest-model branch.
- Unify retry mechanism for juju and maas calls
- Use --format=yaml for any juju call

4c31caf... by Andrea Ieri

Stage pacemaker to allow crm_attribute calls. Bump version.

Reviewed-on: https://code.launchpad.net/~aieri/bootstack-ops/+git/bootstack-ops/+merge/372419
Reviewed-by: David O Neill <email address hidden>

2b6323c... by Andrea Ieri

Stage pacemaker to allow crm_attribute calls. Bump version.

d3bedc2... by Diko Parvanov

Got the backup script from the infra-node charm and updated the one in the ops snap.

Reviewed-on: https://code.launchpad.net/~dparv/bootstack-ops/+git/bootstack-ops/+merge/370549
Reviewed-by: David O Neill <email address hidden>

e293df0... by Andrea Ieri

A set of expect scripts to automate operations on Cisco BMCs

Reviewed-on: https://code.launchpad.net/~aieri/bootstack-ops/+git/bootstack-ops/+merge/371547
Reviewed-by: David O Neill <email address hidden>

d58317d... by Andrea Ieri

Added goliveinfo.sh and portalinfo.sh
Added CDK golive data

7eaaf02... by Andrea Ieri

A set of expect scripts to automate operations on Cisco BMCs

917d27a... by Diko Parvanov

Updated backup script with latest changes

127965c... by Diko Parvanov

Merge branch 'juju-select-largest-model' of https://git.launchpad.net/bootstack-ops

8000536... by James Hebden

Moved to using snapcraft bases and updated for snapcraft 3.0

Reviewed-on: https://code.launchpad.net/~canonical-bootstack/bootstack-ops/+git/bootstack-ops/+merge/367557
Reviewed-by: David O Neill <email address hidden>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/bootstack-ops/cloud_report.py b/bootstack-ops/cloud_report.py
2index c880943..4c4b085 100644
3--- a/bootstack-ops/cloud_report.py
4+++ b/bootstack-ops/cloud_report.py
5@@ -43,8 +43,6 @@ import json
6 import click
7 import subprocess
8 import yaml
9-import re
10-from collections import OrderedDict
11
12
13 from maas.client import login, connect
14@@ -53,7 +51,7 @@ from gevent.pywsgi import WSGIServer
15 from maas.client.viscera.controllers import RackController, RegionController
16 from oauthlib import oauth1
17 import requests
18-from aiohttp.client_exceptions import ServerDisconnectedError
19+from functools import wraps
20
21
22 JUJU_DATA = os.environ.get('JUJU_DATA')
23@@ -69,8 +67,7 @@ API_PORT = os.environ.get("API_PORT", 5000)
24
25 UNKNOWN_VALUE = "unknown"
26
27-RETRY = 10
28-SLEEP = 2
29+NUMBER_OF_RETRIES = 4
30
31
32 def get_timestamp():
33@@ -94,13 +91,38 @@ def url_join(base, uri):
34 return base + uri
35
36
37-def retry(call_to_retry, num=RETRY):
38- for i in range(0, num):
39- try:
40- return call_to_retry()
41- except Exception:
42- time.sleep(SLEEP * i)
43- print("Error: failed to retrieve machines from maas [{}/{}]".format(i+1,num))
44+def retry(exceptions, tries=NUMBER_OF_RETRIES, delay=3, backoff=2, logger=None):
45+ """
46+ Retry calling the decorated function using an exponential backoff.
47+
48+ Args:
49+ exceptions: The exception to check. may be a tuple of
50+ exceptions to check.
51+ tries: Number of times to try (not retry) before giving up.
52+ delay: Initial delay between retries in seconds.
53+ backoff: Backoff multiplier (e.g. value of 2 will double the delay
54+ each retry).
55+ logger: Logger to use. If None, print.
56+ """
57+ def deco_retry(f):
58+ @wraps(f)
59+ def f_retry(*args, **kwargs):
60+ mtries, mdelay = tries, delay
61+ while mtries > 1:
62+ try:
63+ return f(*args, **kwargs)
64+ except exceptions as e:
65+ msg = '{}, Retrying in {} seconds...'.format(e, mdelay)
66+ if logger:
67+ logger.warning(msg)
68+ else:
69+ print(msg)
70+ time.sleep(mdelay)
71+ mtries -= 1
72+ mdelay *= backoff
73+ return None
74+ return f_retry # true decorator
75+ return deco_retry
76
77
78 class MaasV2(object):
79@@ -109,6 +131,7 @@ class MaasV2(object):
80 self.name = "maasv2"
81 self.maas_endpoint = None
82
83+ @retry(Exception)
84 def connect(self):
85 """
86 Connect to Maas controller using credentials extracted from the environemnt variables.
87@@ -133,56 +156,44 @@ class MaasV2(object):
88 else:
89 self.maas_endpoint = MAAS_ENDPOINT
90
91- for i in range(0, RETRY):
92- try:
93- if MAAS_PASSWORD:
94- self.client = login(
95- self.maas_endpoint,
96- username=MAAS_USERNAME, password=MAAS_PASSWORD,
97- )
98- elif MAAS_APIKEY:
99- self.client = connect(
100- self.maas_endpoint,
101- apikey=MAAS_APIKEY
102- )
103- return self.client is not None
104- except Exception as e:
105- print("Error connecting to MAAS endpoint: {}. {}".format(self.maas_endpoint, e))
106- time.sleep(SLEEP * i)
107+ if MAAS_PASSWORD:
108+ self.client = login(
109+ self.maas_endpoint,
110+ username=MAAS_USERNAME, password=MAAS_PASSWORD,
111+ )
112+ elif MAAS_APIKEY:
113+ self.client = connect(
114+ self.maas_endpoint,
115+ apikey=MAAS_APIKEY
116+ )
117+ return self.client is not None
118
119 def disconnect(self):
120 pass
121
122+ @retry(Exception)
123 def _list_machines(self):
124- try:
125- return retry(self.client.machines.list) or []
126- except Exception as e:
127- print("Error retrieving machines list from MAAS endpoint: {}. {}".format(self.maas_endpoint, e))
128- return []
129+ return self.client.machines.list() or []
130
131+ @retry(Exception)
132 def _list_rack_controllers(self):
133- try:
134- return retry(self.client.rack_controllers.list) or []
135- except Exception as e:
136- print("Error retrieving rack controllers list from MAAS endpoint: {}. {}".format(self.maas_endpoint, e))
137- return []
138+ return self.client.rack_controllers.list() or []
139
140+ @retry(Exception)
141 def _list_region_controllers(self):
142- try:
143- return retry(self.client.region_controllers.list) or []
144- except Exception as e:
145- print("Error retrieving region controllers list from MAAS endpoint: {}. {}".format(self.maas_endpoint, e))
146- return []
147+ return self.client.region_controllers.list() or []
148
149+ @retry(Exception)
150 def _get_maas_version(self):
151 version = "None"
152- report_version = retry(self.client.version.get)
153+ report_version = self.client.version.get()
154 if report_version:
155 version = report_version.version
156 return version
157
158+ @retry(Exception)
159 def _get_power_parameters(self, m):
160- return retry(m.get_power_parameters) or {}
161+ return m.get_power_parameters() or {}
162
163 def _serialize_machine(self, m):
164 """
165@@ -227,9 +238,19 @@ class MaasV2(object):
166
167 maas_version = self._get_maas_version()
168
169- machines = list(map(self._serialize_machine, self._list_machines())) + \
170- list(map(self._serialize_machine, self._list_rack_controllers())) + \
171- list(map(self._serialize_machine, self._list_region_controllers()))
172+ machines = []
173+
174+ maas_machine_list = self._list_machines()
175+ if maas_machine_list:
176+ machines += list(map(self._serialize_machine, maas_machine_list))
177+
178+ rack_controller_list = self._list_rack_controllers()
179+ if rack_controller_list:
180+ machines += list(map(self._serialize_machine, rack_controller_list))
181+
182+ region_controller_list = self._list_region_controllers()
183+ if region_controller_list:
184+ machines += list(map(self._serialize_machine, region_controller_list))
185
186 machine_ids = set()
187 maas_machines = list()
188@@ -292,22 +313,20 @@ class MaasV1(object):
189 def disconnect(self):
190 pass
191
192+ @retry(Exception)
193 def _perform_request(self, uri):
194- try:
195- client = oauth1.Client(self.consumer_key,
196- client_secret='',
197- resource_owner_key=self.key,
198- resource_owner_secret=self.secret,
199- signature_method=oauth1.SIGNATURE_PLAINTEXT)
200-
201- url = url_join(self.maas_endpoint, uri)
202- uri, headers, body = retry(client.sign(url)) or [None, None, None]
203- response = retry(requests.get(uri, headers=headers)) or ""
204- if response.status_code == 200:
205- return response.json()
206- except Exception as e:
207- print("Error querying maas {}, {}".format(url, e))
208-
209+ client = oauth1.Client(self.consumer_key,
210+ client_secret='',
211+ resource_owner_key=self.key,
212+ resource_owner_secret=self.secret,
213+ signature_method=oauth1.SIGNATURE_PLAINTEXT)
214+
215+ url = url_join(self.maas_endpoint, uri)
216+ uri, headers, body = client.sign(url) or [None, None, None]
217+ response = requests.get(uri, headers=headers) or ""
218+
219+ if response and response.status_code == 200:
220+ return response.json()
221 return {}
222
223 def _get_power_address(self, system_id):
224@@ -382,35 +401,38 @@ class Juju(object):
225 self.models = []
226 self.loop = asyncio.get_event_loop()
227
228- def disconnect(self):
229- pass
230+ if os.path.isfile('/snap/bin/juju'):
231+ self.juju_cmd = ['/snap/bin/juju']
232+ elif os.path.isfile("/usr/bin/juju"):
233+ self.juju_cmd = ['/usr/bin/juju']
234+ else:
235+ self.juju_cmd = ['snap', 'run', 'juju']
236
237- def _list_machines(self):
238 if not JUJU_DATA:
239 print("Please export JUJU_DATA env variable before running the script")
240 sys.exit(1)
241
242+ self.env = os.environ.copy()
243+ self.env["JUJU_DATA"] = JUJU_DATA
244+
245+ def disconnect(self):
246+ pass
247+
248+ @retry(Exception)
249+ def run_juju_command(self, *args):
250+ return subprocess.check_output(self.juju_cmd + list(args), timeout=60, env=self.env)
251+
252+ def _list_machines(self, model=None):
253 machines = []
254- env = os.environ.copy()
255- env["JUJU_DATA"] = JUJU_DATA
256
257- if os.path.isfile('/snap/bin/juju'):
258- cmd = ['/snap/bin/juju', 'status', '--format=yaml']
259- elif os.path.isfile("/usr/bin/juju"):
260- cmd = ['/usr/bin/juju', 'status', '--format=yaml']
261- else:
262- cmd = ['snap run juju', 'status', '--format=yaml']
263-
264- for i in range(0, RETRY):
265- try:
266- output = subprocess.check_output(cmd, timeout=30, env=env)
267- parsed = yaml.safe_load(output)
268- machines = parsed['machines']
269- i = RETRY + 1
270- except Exception as e:
271- print("Error: failed to retrieve machines from juju [{}/{}]\n{}".format(i+1,RETRY,e))
272- time.sleep(SLEEP * i)
273-
274+ cmd = ['status', '--format=yaml']
275+ if model:
276+ cmd.append('-m{}'.format(model))
277+
278+ output = self.run_juju_command(*cmd)
279+ if output:
280+ parsed = yaml.safe_load(output)
281+ machines = parsed.get('machines', [])
282 return machines
283
284 def connect(self):
285@@ -486,38 +508,18 @@ class Juju(object):
286 Switches to a given model
287 :param model: the model to switch to
288 """
289-
290- if os.path.isfile('/snap/bin/juju'):
291- cmd = ['/snap/bin/juju', 'switch', model]
292- elif os.path.isfile("/usr/bin/juju"):
293- cmd = ['/usr/bin/juju', 'switch', model]
294- else:
295- cmd = ['snap run juju', 'switch', model]
296-
297- try:
298- subprocess.check_output(cmd)
299- except subprocess.CalledProcessError as e:
300- print("Exception calling juju status command. {}".format(e))
301+ self.run_juju_command('switch', model)
302
303 def list_models(self):
304 """
305 Gets the output of juju list-models
306- :returns dict: the above format
307 """
308- models = None
309-
310- if os.path.isfile('/snap/bin/juju'):
311- cmd = ['/snap/bin/juju', 'list-models']
312- elif os.path.isfile("/usr/bin/juju"):
313- cmd = ['/usr/bin/juju', 'list-models']
314- else:
315- cmd = ['snap run juju', 'list-models']
316-
317- try:
318- models = subprocess.check_output(cmd).decode("utf-8")
319- except subprocess.CalledProcessError as e:
320- print("Exception calling juju status command. {}".format(e))
321+ models = []
322+ output = self.run_juju_command('list-models', '--format=yaml')
323
324+ if output:
325+ parsed = yaml.safe_load(output)
326+ models = parsed.get('models', [])
327 return models
328
329 def get_model_and_sizes(self):
330@@ -530,29 +532,12 @@ class Juju(object):
331 }
332 :returns dict: the above format
333 """
334- models = self.list_models().split('\n')
335- modelsDict = {}
336- machinesColnum = 0
337-
338- for line in models:
339- if line.strip() == "":
340- continue
341-
342- if line.startswith('Controller') and machinesColnum == 0:
343- continue
344-
345- line = line.replace('\n', ' ').replace('\r', '')
346- cols = re.split('\s{2,100}', line)
347-
348- if machinesColnum == 0:
349- for col in cols:
350- if col == "Machines":
351- break
352- machinesColnum += 1
353- else:
354- modelsDict[cols[0]] = int(cols[machinesColnum])
355-
356- return modelsDict
357+ result = {}
358+ models = self.list_models()
359+ for model in models:
360+ model_name = model['short-name']
361+ result[model_name] = len(self._list_machines(model=model_name))
362+ return result
363
364 def switch_to_largest_model(self):
365 """
366@@ -566,22 +551,19 @@ class Juju(object):
367 Then calls switch_model
368 """
369 largest = -1
370- largestModel = None
371+ largest_model = None
372 models = self.get_model_and_sizes()
373
374 if len(models) == 0:
375- print("No models found ?????")
376+ print("Error, no models found")
377 return
378
379- for k,v in models.items():
380+ for k, v in models.items():
381 if v > largest:
382 largest = v
383- largestModel = k
384+ largest_model = k
385
386- if "*" not in largestModel:
387- self.switch_model(largestModel)
388- else:
389- print("Largest model already selected: " + largestModel)
390+ self.switch_model(largest_model)
391
392 def get_report(self, running=False):
393 """

Subscribers

People subscribed via source and target branches

to all changes: