Merge bootstack-ops:create_patch_groups_using_AZ into bootstack-ops:master

Proposed by Paul Henien
Status: Rejected
Rejected by: Haw Loeung
Proposed branch: bootstack-ops:create_patch_groups_using_AZ
Merge into: bootstack-ops:master
Diff against target: 287 lines (+281/-0)
1 file modified
bootstack-ops/create_patch_groups.py (+281/-0)
Reviewer Review Type Date Requested Status
Giuseppe Petralia Needs Fixing
Review via email: mp+373145@code.launchpad.net

Commit message

Added script called "create_patch_groups.py"

Description of the change

Script retrieves node details from maas and juju, sorts the nodes into patch groups depending on availability zone and creates a upgrade profile in landscape for each patch group.

Script has a --help flag for instructions on how to run the script.
Can be tested using the --dry-run flag which outputs the patch groups but doesn't create upgrade profiles in landscape.

Requirements:
landscape-api and yaml packages needs to be installed
yaml file containing juju status format yaml
yaml file called landscape_creds.yml which contains landscape credentials

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 :

Please see my comments inline.

review: Needs Fixing

Unmerged commits

bcd9d20... by Paul Henien

added comments

42312df... by Paul Henien

modified script to take jsfy file as argument

a864500... by Paul Henien

added create_patch_groups.py script

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>

3ec3503... by James <email address hidden>

Readded libc6

3759b88... by James <email address hidden>

Moved to using snapcraft bases and updated for snapcraft 3.0

93070a0... by Wouter van Bommel

Added a maas test script for network testing

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

906f248... by Giuseppe Petralia

Switch to controller model before launching juju create-backup

Reviewed-on: https://code.launchpad.net/~peppepetra86/bootstack-ops/+git/bootstack-ops/+merge/362453
Reviewed-by: David O Neill <email address hidden>
Reviewed-by: Alok G Singh <email address hidden>

58aaa04... by Giuseppe Petralia

Add sleep retry on MAAS connection errors

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

7a73b9b... by Drew Freiberger

Updated openstack_managed_upgrades.py to specify phases matching openstack upgrade timing template

Reviewed-on: https://code.launchpad.net/~afreiberger/bootstack-ops/+git/bootstack-ops/+merge/367075
Reviewed-by: James Hebden <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/create_patch_groups.py b/bootstack-ops/create_patch_groups.py
2new file mode 100755
3index 0000000..b00a937
4--- /dev/null
5+++ b/bootstack-ops/create_patch_groups.py
6@@ -0,0 +1,281 @@
7+#!/usr/bin/env python2.7
8+
9+"""
10+# create_patch_groups.py
11+
12+Retrieve maas machines and juju containers, sort into patch groups and create a upgrade profile in landscape for each patch group
13+
14+## Version History
15+
16+
17+### 0.1.0
18+
19+* Initial release
20+
21+"""
22+
23+__version__ = "0.1.0"
24+__author__ = "Paul Henien <paul.henien@canonical.com>"
25+
26+import subprocess
27+import json
28+import os
29+import sys
30+import yaml
31+import argparse
32+import time
33+# Check that landscape-api package is installed
34+try:
35+ from landscape_api.base import API, HTTPError
36+except ImportError:
37+ sys.exit("""Please install the package 'Landscape API Python module' before continuing.""")
38+
39+
40+def get_nodes():
41+ # Execute "maas list" to grab the username to interact with maas
42+ try:
43+ user_name = subprocess.check_output("maas list | awk '{print $1}'", shell=True).decode('ascii').strip()
44+ except:
45+ print("Please check user exists")
46+ # Execute "maas list" to grab the api version
47+ api_version = ((subprocess.check_output("maas list | awk '{print $2}'", shell=True).decode('ascii').strip()).split('/'))[5]
48+ # Check if api version 1.0 or 2.0 is used and execute node retrieval
49+ # command based on the api version
50+ if api_version == '1.0':
51+ nodes = subprocess.check_output('maas {} nodes list'.format(user_name), shell=True).decode('ascii').strip()
52+ elif api_version == '2.0':
53+ nodes = subprocess.check_output('maas {} machines read'.format(user_name), shell=True).decode('ascii').strip()
54+ return nodes
55+
56+
57+def nodes_in_az(nodes):
58+ # Converts node data into json
59+ nodes_json = json.loads(nodes)
60+ # Initialise variable to store zones and the nodes assigned
61+ zones = dict()
62+ # Run through each node
63+ for x in nodes_json:
64+ # "key" is the zone name that is used as a key in the dictionary
65+ key = x['zone']['resource_uri'].split('/')[5]
66+ # setdefault checks if the key exists,
67+ # if key doesn't exist in the dictionary it will add it,
68+ # if not it will append the value to the existing key
69+ zones.setdefault(key, [])
70+ zones[key].append(x['hostname'])
71+ return zones
72+
73+
74+def landscape_auth():
75+ # Authenticate to landscape using credentials from landscape_creds.yml and return authentication token
76+ try: # Check if landscape_creds.yml file exists
77+ with open("landscape_creds.yml", 'r') as ymlfile:
78+ cfg = yaml.safe_load(ymlfile)
79+ except IOError:
80+ print("Please provide a valid landscape credentials file")
81+ sys.exit(1)
82+
83+ api = API(cfg['uri'], cfg['key'], cfg['secret'], cfg['ca'])
84+
85+ return api
86+
87+
88+def landscape_computers(api, nodes):
89+ # Retrieve a list of the computers in landscaoe and append them to a list called ls_computers
90+ try:
91+ computers = api.get_computers()
92+ pass
93+ except HTTPError, e:
94+ print ("\nGot server error:\n"
95+ "Code: %d\n"
96+ "Message: %s\n") % (e.code, e.message)
97+ sys.exit(1)
98+
99+ ls_computers = []
100+ for computer in computers:
101+ ls_computers.append(computer['title'].encode("utf-8"))
102+
103+ print "Computers in landscape: {}".format(ls_computers)
104+ print ""
105+
106+ return ls_computers
107+
108+
109+def multiple_az_into_groups(YAML, args, PATCH_GROUPS, ls_computers):
110+
111+ if args.application is None: # Check if any applications are added in the excluded flag
112+ excluded = []
113+ else:
114+ excluded = args.application.lower().split(',')
115+ print("Excluded: {}".format(excluded))
116+ print("")
117+
118+ APPS = YAML["applications"]
119+
120+ for APP in APPS:
121+ if APP not in excluded: # If application hasn't been excluded, nodes are added to patch groups
122+ try:
123+ UNITS = []
124+ counter = 0
125+ for UNIT in APPS[APP]['units']:
126+ UNITS.append(UNIT)
127+ print("{} : {}".format(APP, UNITS))
128+ except:
129+ print("{} : No units".format(APP))
130+
131+
132+ for machine in YAML['machines']:
133+ try:
134+ for container in YAML['machines'][machine]['containers']:
135+ for APP in APPS:
136+ if APP not in excluded:
137+ try:
138+ for UNIT in APPS[APP]['units']:
139+ if APPS[APP]['units'][UNIT]['machine'] == container:
140+ a, zone = YAML['machines'][machine]['containers'][container]['hardware'].split("=")
141+ if YAML['machines'][machine]['containers'][container]['instance-id'] in ls_computers:
142+ PATCH_GROUPS[zone].append(YAML['machines'][machine]['containers'][container]['instance-id'])
143+ except:
144+ pass
145+ except:
146+ pass
147+
148+ PG = []
149+
150+ for key, value in PATCH_GROUPS.items():
151+ counter = 0
152+ GROUP = []
153+ for i in value:
154+ if i in ls_computers:
155+ GROUP.append(i.encode("utf-8"))
156+ counter += 1
157+ PG.append(GROUP)
158+
159+ print ""
160+ print "PG 1: {}".format(PG[0])
161+ print "PG 2: {}".format(PG[1])
162+ print "PG 3: {}".format(PG[2])
163+
164+ return PATCH_GROUPS
165+
166+
167+def single_az_into_groups(YAML, args, nodes, ls_computers):
168+ PATCH_GROUPS = [[], [], []]
169+ counter = 0
170+ for key, value in nodes.items():
171+ for i in value:
172+ if i in ls_computers: # Check if node exists in landscape
173+ PATCH_GROUPS[counter % 3].append(i.encode("utf-8"))
174+ counter += 1
175+
176+ if args.application is None:
177+ excluded = []
178+ else:
179+ excluded = args.application.lower().split(',')
180+ print("Excluded: {}".format(excluded))
181+ print("")
182+
183+ APPS = YAML["applications"]
184+
185+ for APP in APPS:
186+ if APP not in excluded:
187+ try:
188+ UNITS = []
189+ counter = 0
190+ for UNIT in APPS[APP]['units']:
191+ for machine in YAML['machines']:
192+ try:
193+ for container in YAML['machines'][machine]['containers']:
194+ if APPS[APP]['units'][UNIT]['machine'] == container:
195+ HOSTNAME = YAML['machines'][machine]['containers'][container]['instance-id']
196+ UNITS.append(HOSTNAME)
197+ if HOSTNAME not in PATCH_GROUPS:
198+ if HOSTNAME in ls_computers:
199+ PATCH_GROUPS[counter % 3].append(HOSTNAME) # Distributes hosts in application over three patch groups
200+ counter += 1
201+ except:
202+ pass
203+ print("{} : {}".format(APP, UNITS))
204+ except:
205+ print("{} : No units".format(APP))
206+
207+ print ""
208+ print "PATCH_GROUP1: {}".format(PATCH_GROUPS[0])
209+ print "PATCH_GROUP2: {}".format(PATCH_GROUPS[1])
210+ print "PATCH_GROUP3: {}".format(PATCH_GROUPS[2])
211+
212+ return PATCH_GROUPS
213+
214+
215+"""
216+ def remove_duplicates(PATCH_GROUPS):
217+ PG = []
218+ for group in PATCH_GROUPS:
219+ PG.append(list(dict.fromkeys(group)))
220+
221+ print ""
222+ print "PATCH_GROUP1: {}".format(PG[0])
223+ print "PATCH_GROUP2: {}".format(PG[1])
224+ print "PATCH_GROUP3: {}".format(PG[2])
225+
226+ return PG
227+"""
228+
229+
230+def create_upgrade_profile(api, PATCH_GROUPS):
231+ # Takes patch groups and creates upgrade profiles in landscape for each one
232+ upgrade_profiles = api.get_upgrade_profiles()
233+ profiles = []
234+ for profile in upgrade_profiles:
235+ profiles.append(profile["name"])
236+
237+ counter = 1
238+ for group in PATCH_GROUPS.keys():
239+ try:
240+ if "patch_group{}".format(counter) in profiles:
241+ up_pro = "patch_group{}".format(counter)
242+ api.remove_upgrade_profile(name=up_pro)
243+
244+ api.create_upgrade_profile(title="PATCH_GROUP{}".format(counter),every="week",on_days=["we","th"],at_hour=20,at_minute=0,upgrade_type="all")
245+ for machine in PATCH_GROUPS[group]:
246+ api.add_tags_to_computers(query=machine, tags="PATCH_GROUP{}".format(counter))
247+ api.associate_upgrade_profile(name="patch_group{}".format(counter),tags="PATCH_GROUP{}".format(counter))
248+ except HTTPError, e:
249+ print ("\nGot server error:\n"
250+ "Code: %d\n"
251+ "Message: %s\n") % (e.code, e.message)
252+ sys.exit(1)
253+
254+ counter += 1
255+
256+
257+def main():
258+ parser = argparse.ArgumentParser(description="Create patch groups from maas kvms and juju lxds")
259+ parser.add_argument("--exclude", help="exclude juju application. Format: app1,app2,app3", dest="application", type=str, required=False)
260+ parser.add_argument("--dry-run", help="only lists patch groups but doesn't create them", action="store_true", default=False)
261+ parser.add_argument("--jsfy-file", help="juju status format yaml file path (run juju status --format yaml > jsfy to create file) ", dest="jsfy", required=True)
262+ args = parser.parse_args()
263+ try:
264+ with open(args.jsfy) as myjsfy:
265+ YAML = yaml.safe_load(myjsfy)
266+ except IOError:
267+ print("Please provide a valid jsfy file")
268+ sys.exit(1)
269+
270+ nodes = get_nodes()
271+ az_node_assignment = nodes_in_az(nodes)
272+ api = landscape_auth()
273+ ls_computers = landscape_computers(api, az_node_assignment)
274+
275+ # Check if cloud has multiple AZs
276+ if len(az_node_assignment) > 1:
277+ PATCH_GROUPS = multiple_az_into_groups(YAML, args, az_node_assignment, ls_computers)
278+ else:
279+ PATCH_GROUPS = single_az_into_groups(YAML, args, az_node_assignment, ls_computers)
280+ # PATCH_GROUPS = remove_duplicates(PATCH_GROUPS)
281+ # If dry_run flag is not set, create patch groups
282+ if args.dry_run == False:
283+ create_upgrade_profile(api, PATCH_GROUPS)
284+
285+
286+if __name__ == "__main__":
287+ main()

Subscribers

People subscribed via source and target branches

to all changes: