Merge ~afreiberger/charm-glance-sync:blacken-20.08 into charm-glance-sync:master

Proposed by Drew Freiberger
Status: Merged
Merged at revision: b37ab82942fd8266a0998a1ce591aef755fc70de
Proposed branch: ~afreiberger/charm-glance-sync:blacken-20.08
Merge into: charm-glance-sync:master
Prerequisite: ~afreiberger/charm-glance-sync:makefile-20.08
Diff against target: 2500 lines (+756/-712)
10 files modified
src/files/check_stale_lockfile_master.py (+18/-13)
src/files/check_stale_lockfile_slave.py (+18/-13)
src/files/db_purge_deleted_master/db_purge_deleted_glance_images.py (+20/-19)
src/files/db_purge_deleted_slave/db_purge_deleted_glance_images.py (+11/-11)
src/files/glance_sync_master.py (+94/-81)
src/files/glance_sync_slave.py (+280/-231)
src/reactive/glance_sync.py (+282/-314)
src/tests/functional/tests/test_glance_sync.py (+29/-26)
src/tests/unit/__init__.py (+2/-1)
src/tox.ini (+2/-3)
Reviewer Review Type Date Requested Status
Xav Paice (community) Approve
Review via email: mp+388630@code.launchpad.net

Commit message

Blackened repository to 88 lines and fixed up lint

To post a comment you must log in.
Revision history for this message
Xav Paice (xavpaice) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/files/check_stale_lockfile_master.py b/src/files/check_stale_lockfile_master.py
index bd6aa3d..341441d 100644
--- a/src/files/check_stale_lockfile_master.py
+++ b/src/files/check_stale_lockfile_master.py
@@ -5,20 +5,25 @@ import os.path
5import sys5import sys
6import time6import time
77
8('Check the status of lock file to be sure it is not stale, '8(
9 'warn at 7200 seconds crit at 14400 seconds')9 "Check the status of lock file to be sure it is not stale, "
10 "warn at 7200 seconds crit at 14400 seconds"
11)
1012
11parser = optparse.OptionParser()13parser = optparse.OptionParser()
12parser.add_option(14parser.add_option(
13 '-w', action='store', default='7200', help='seconds to warn', type='int')15 "-w", action="store", default="7200", help="seconds to warn", type="int"
16)
14parser.add_option(17parser.add_option(
15 '-c', action='store', default='14400', help='seconds to crit', type='int')18 "-c", action="store", default="14400", help="seconds to crit", type="int"
19)
16parser.add_option(20parser.add_option(
17 '-f',21 "-f",
18 action='store',22 action="store",
19 default='/tmp/glance_sync_master.lock',23 default="/tmp/glance_sync_master.lock",
20 help='file to check',24 help="file to check",
21 type='string')25 type="string",
26)
2227
23options, args = parser.parse_args()28options, args = parser.parse_args()
2429
@@ -29,7 +34,7 @@ nagCrit = 2
29try:34try:
30 statInfo = os.stat(options.f)35 statInfo = os.stat(options.f)
31except OSError:36except OSError:
32 print('OK: lockfile {} not present'.format(options.f))37 print("OK: lockfile {} not present".format(options.f))
33 sys.exit(nagOk)38 sys.exit(nagOk)
3439
35now = int(time.time())40now = int(time.time())
@@ -37,11 +42,11 @@ statInfoSlice = statInfo[8]
37timeDiff = now - statInfoSlice42timeDiff = now - statInfoSlice
3843
39if timeDiff > options.c:44if timeDiff > options.c:
40 print('CRIT: lock file is older than {} seconds'.format(options.c))45 print("CRIT: lock file is older than {} seconds".format(options.c))
41 sys.exit(nagCrit)46 sys.exit(nagCrit)
42elif timeDiff > options.w:47elif timeDiff > options.w:
43 print('WARN: lock file is older than {} seconds'.format(options.w))48 print("WARN: lock file is older than {} seconds".format(options.w))
44 sys.exit(nagWarn)49 sys.exit(nagWarn)
45else:50else:
46 print('OK: lock file is under 3 hours')51 print("OK: lock file is under 3 hours")
47 sys.exit(nagOk)52 sys.exit(nagOk)
diff --git a/src/files/check_stale_lockfile_slave.py b/src/files/check_stale_lockfile_slave.py
index 1be8b9b..08412be 100644
--- a/src/files/check_stale_lockfile_slave.py
+++ b/src/files/check_stale_lockfile_slave.py
@@ -5,20 +5,25 @@ import os.path
5import sys5import sys
6import time6import time
77
8('Check the status of lock file to be sure it is not stale, '8(
9 'warn at 7200 seconds crit at 14400 seconds')9 "Check the status of lock file to be sure it is not stale, "
10 "warn at 7200 seconds crit at 14400 seconds"
11)
1012
11parser = optparse.OptionParser()13parser = optparse.OptionParser()
12parser.add_option(14parser.add_option(
13 '-w', action='store', default='7200', help='seconds to warn', type='int')15 "-w", action="store", default="7200", help="seconds to warn", type="int"
16)
14parser.add_option(17parser.add_option(
15 '-c', action='store', default='14400', help='seconds to crit', type='int')18 "-c", action="store", default="14400", help="seconds to crit", type="int"
19)
16parser.add_option(20parser.add_option(
17 '-f',21 "-f",
18 action='store',22 action="store",
19 default='/tmp/glance_sync_slave.lock',23 default="/tmp/glance_sync_slave.lock",
20 help='file to check',24 help="file to check",
21 type='string')25 type="string",
26)
2227
23options, args = parser.parse_args()28options, args = parser.parse_args()
2429
@@ -29,7 +34,7 @@ nagCrit = 2
29try:34try:
30 statInfo = os.stat(options.f)35 statInfo = os.stat(options.f)
31except OSError:36except OSError:
32 print('OK: lockfile {} not present'.format(options.f))37 print("OK: lockfile {} not present".format(options.f))
33 sys.exit(nagOk)38 sys.exit(nagOk)
3439
35now = int(time.time())40now = int(time.time())
@@ -37,11 +42,11 @@ statInfoSlice = statInfo[8]
37timeDiff = now - statInfoSlice42timeDiff = now - statInfoSlice
3843
39if timeDiff > options.c:44if timeDiff > options.c:
40 print('CRIT: lock file is older than {} seconds'.format(options.c))45 print("CRIT: lock file is older than {} seconds".format(options.c))
41 sys.exit(nagCrit)46 sys.exit(nagCrit)
42elif timeDiff > options.w:47elif timeDiff > options.w:
43 print('WARN: lock file is older than {} seconds'.format(options.w))48 print("WARN: lock file is older than {} seconds".format(options.w))
44 sys.exit(nagWarn)49 sys.exit(nagWarn)
45else:50else:
46 print('OK: lock file is under 3 hours')51 print("OK: lock file is under 3 hours")
47 sys.exit(nagOk)52 sys.exit(nagOk)
diff --git a/src/files/db_purge_deleted_master/db_purge_deleted_glance_images.py b/src/files/db_purge_deleted_master/db_purge_deleted_glance_images.py
index 144383f..bbc97c6 100644
--- a/src/files/db_purge_deleted_master/db_purge_deleted_glance_images.py
+++ b/src/files/db_purge_deleted_master/db_purge_deleted_glance_images.py
@@ -30,27 +30,28 @@ config = {}
3030
31# pull connection information from glance-api.conf31# pull connection information from glance-api.conf
32try:32try:
33 with open('/etc/glance/glance-api.conf', 'r') as conf_file:33 with open("/etc/glance/glance-api.conf", "r") as conf_file:
34 for line in conf_file.readlines():34 for line in conf_file.readlines():
35 if line.startswith('connection ='):35 if line.startswith("connection ="):
36 # connection = mysql://glance:<password>@<host>/glance36 # connection = mysql://glance:<password>@<host>/glance
37 connection = urlparse.urlparse(line.split('=')[1].strip())37 connection = urlparse.urlparse(line.split("=")[1].strip())
38 config['host'] = connection.hostname38 config["host"] = connection.hostname
39 config['user'] = connection.username39 config["user"] = connection.username
40 config['password'] = connection.password40 config["password"] = connection.password
41 config['database'] = connection.path[1:].strip()41 config["database"] = connection.path[1:].strip()
42 break42 break
43except IOError as e:43except IOError as e:
44 sys.exit(e)44 sys.exit(e)
4545
46try:46try:
47 connection = MySQLdb.connect(47 connection = MySQLdb.connect(
48 host=config['host'],48 host=config["host"],
49 user=config['user'],49 user=config["user"],
50 passwd=config['password'],50 passwd=config["password"],
51 db=config['database'])51 db=config["database"],
52 )
52except Exception as e:53except Exception as e:
53 print('ERROR: unable to connect to mysql database')54 print("ERROR: unable to connect to mysql database")
54 sys.exit(e)55 sys.exit(e)
5556
56cursor = connection.cursor()57cursor = connection.cursor()
@@ -60,14 +61,14 @@ cursor.execute("SELECT id FROM glance.images WHERE status='deleted';")
60image_ids = cursor.fetchall()61image_ids = cursor.fetchall()
6162
62for image_id in image_ids:63for image_id in image_ids:
63 print('purging {}'.format(image_id[0]))64 print("purging {}".format(image_id[0]))
64 args = (image_id[0])65 args = image_id[0]
65 commands = [66 commands = [
66 'DELETE FROM glance.image_properties WHERE image_id=%s;',67 "DELETE FROM glance.image_properties WHERE image_id=%s;",
67 'DELETE FROM glance.image_members WHERE image_id=%s;',68 "DELETE FROM glance.image_members WHERE image_id=%s;",
68 'DELETE FROM glance.image_tags WHERE image_id=%s;',69 "DELETE FROM glance.image_tags WHERE image_id=%s;",
69 'DELETE FROM glance.image_locations WHERE image_id=%s;',70 "DELETE FROM glance.image_locations WHERE image_id=%s;",
70 'DELETE FROM glance.images WHERE id=%s;'71 "DELETE FROM glance.images WHERE id=%s;",
71 ]72 ]
7273
73 for command in commands:74 for command in commands:
diff --git a/src/files/db_purge_deleted_slave/db_purge_deleted_glance_images.py b/src/files/db_purge_deleted_slave/db_purge_deleted_glance_images.py
index c0acc47..74f57be 100644
--- a/src/files/db_purge_deleted_slave/db_purge_deleted_glance_images.py
+++ b/src/files/db_purge_deleted_slave/db_purge_deleted_glance_images.py
@@ -27,10 +27,11 @@ from contextlib import closing
27import mysql.connector as mysql27import mysql.connector as mysql
2828
29con = mysql.connect(29con = mysql.connect(
30 host=os.environ['OS_MYSQL_HOST'],30 host=os.environ["OS_MYSQL_HOST"],
31 user=os.environ['OS_MYSQL_USER'],31 user=os.environ["OS_MYSQL_USER"],
32 password=os.environ['OS_MYSQL_PASS'],32 password=os.environ["OS_MYSQL_PASS"],
33 database=os.environ['OS_MYSQL_DB'])33 database=os.environ["OS_MYSQL_DB"],
34)
3435
35# delete from images where status = 'deleted'36# delete from images where status = 'deleted'
36with closing(con.cursor()) as cur:37with closing(con.cursor()) as cur:
@@ -38,15 +39,14 @@ with closing(con.cursor()) as cur:
38 cur.execute(sql)39 cur.execute(sql)
39 image_ids = cur.fetchall()40 image_ids = cur.fetchall()
40 for image_id in image_ids:41 for image_id in image_ids:
41 print('purging {}'.format(image_id[0]))42 print("purging {}".format(image_id[0]))
42 args = (image_id[0])43 args = image_id[0]
43 commands = [44 commands = [
44 "DELETE FROM glance.image_properties WHERE image_id='{}';".format(45 "DELETE FROM glance.image_properties WHERE image_id='{}';".format(args),
45 args), "DELETE FROM glance.image_members WHERE image_id='{}';".46 "DELETE FROM glance.image_members WHERE image_id='{}';".format(args),
46 format(args),
47 "DELETE FROM glance.image_tags WHERE image_id='{}';".format(args),47 "DELETE FROM glance.image_tags WHERE image_id='{}';".format(args),
48 "DELETE FROM glance.image_locations WHERE image_id='{}';".format(48 "DELETE FROM glance.image_locations WHERE image_id='{}';".format(args),
49 args), "DELETE FROM glance.images WHERE id='{}';".format(args)49 "DELETE FROM glance.images WHERE id='{}';".format(args),
50 ]50 ]
5151
52 for command in commands:52 for command in commands:
diff --git a/src/files/glance_sync_master.py b/src/files/glance_sync_master.py
index 57139b3..5cd53b5 100755
--- a/src/files/glance_sync_master.py
+++ b/src/files/glance_sync_master.py
@@ -20,31 +20,33 @@ from keystoneclient.v3 import client as keystone_v3_client
2020
21def get_keystone_client():21def get_keystone_client():
22 # We know that we set OS_AUTH_VERSION, so use it (and cast to int)22 # We know that we set OS_AUTH_VERSION, so use it (and cast to int)
23 if int(os.environ['OS_AUTH_VERSION']) == 3:23 if int(os.environ["OS_AUTH_VERSION"]) == 3:
24 ksc = keystone_v3_client.Client(24 ksc = keystone_v3_client.Client(
25 auth_url=os.environ['OS_AUTH_URL'],25 auth_url=os.environ["OS_AUTH_URL"],
26 username=os.environ['OS_USERNAME'],26 username=os.environ["OS_USERNAME"],
27 password=os.environ['OS_PASSWORD'],27 password=os.environ["OS_PASSWORD"],
28 user_domain_name=os.environ['OS_USER_DOMAIN_NAME'],28 user_domain_name=os.environ["OS_USER_DOMAIN_NAME"],
29 project_domain_name=os.environ['OS_PROJECT_DOMAIN_NAME'],29 project_domain_name=os.environ["OS_PROJECT_DOMAIN_NAME"],
30 project_name=os.environ['OS_PROJECT_NAME'])30 project_name=os.environ["OS_PROJECT_NAME"],
31 )
31 else:32 else:
32 ksc = keystone_v2_client.Client(33 ksc = keystone_v2_client.Client(
33 username=os.environ['OS_USERNAME'],34 username=os.environ["OS_USERNAME"],
34 password=os.environ['OS_PASSWORD'],35 password=os.environ["OS_PASSWORD"],
35 tenant_name=os.environ['OS_TENANT_NAME'],36 tenant_name=os.environ["OS_TENANT_NAME"],
36 auth_url=os.environ['OS_AUTH_URL'])37 auth_url=os.environ["OS_AUTH_URL"],
38 )
37 return ksc39 return ksc
3840
3941
40def get_glance_client(ksc):42def get_glance_client(ksc):
41 # create a glance client, using the provided keystone client for details43 # create a glance client, using the provided keystone client for details
42 token = ksc.auth_token44 token = ksc.auth_token
43 service = ksc.services.find(name='glance')45 service = ksc.services.find(name="glance")
44 endpoint = ksc.endpoints.find(service_id=service.id, interface='internal')46 endpoint = ksc.endpoints.find(service_id=service.id, interface="internal")
45 glance_url = endpoint.url47 glance_url = endpoint.url
4648
47 return GlanceClient('2', endpoint=glance_url, token=token)49 return GlanceClient("2", endpoint=glance_url, token=token)
4850
4951
50class BootStackMetadataError(Exception):52class BootStackMetadataError(Exception):
@@ -54,14 +56,15 @@ class BootStackMetadataError(Exception):
54 helps map different tenants between regions (with56 helps map different tenants between regions (with
55 different tenant-id, but same/similar tenant-name57 different tenant-id, but same/similar tenant-name
56 """58 """
59
57 pass60 pass
5861
5962
60class ImageSyncMaster:63class ImageSyncMaster:
61 def __init__(self, data_dir='/srv/glance_master_sync/data'):64 def __init__(self, data_dir="/srv/glance_master_sync/data"):
62 self.tenants = {}65 self.tenants = {}
63 self.DATA_DIR = data_dir66 self.DATA_DIR = data_dir
64 tmp_dir = '/tmp/glance_master_sync'67 tmp_dir = "/tmp/glance_master_sync"
65 if not os.path.isdir(tmp_dir):68 if not os.path.isdir(tmp_dir):
66 os.makedirs(tmp_dir)69 os.makedirs(tmp_dir)
6770
@@ -71,76 +74,81 @@ class ImageSyncMaster:
71 def glance_connect(self):74 def glance_connect(self):
72 try:75 try:
7376
74 self.log('connecting to Keystone')77 self.log("connecting to Keystone")
75 self.keystone = get_keystone_client()78 self.keystone = get_keystone_client()
76 except Exception as e:79 except Exception as e:
77 self.log('EXCEPTION: {0}'.format(e))80 self.log("EXCEPTION: {0}".format(e))
78 sys.exit(2)81 sys.exit(2)
79 if not self.tenants:82 if not self.tenants:
80 # In the call to keystone we know that we get a list response.83 # In the call to keystone we know that we get a list response.
81 # 1st element is the response code, 2nd is the data in dicts form84 # 1st element is the response code, 2nd is the data in dicts form
82 self.tenants = dict(85 self.tenants = dict(
83 [(tenant['id'], tenant['name'])86 [
84 for tenant in self.keystone.get('/projects')[1]['projects']87 (tenant["id"], tenant["name"])
85 if tenant['enabled']])88 for tenant in self.keystone.get("/projects")[1]["projects"]
89 if tenant["enabled"]
90 ]
91 )
86 self.glance = get_glance_client(self.keystone)92 self.glance = get_glance_client(self.keystone)
87 return self.glance93 return self.glance
8894
89 def timestamp_now(self):95 def timestamp_now(self):
90 return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')96 return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
9197
92 def log(self, msg):98 def log(self, msg):
93 print('{0} {1}'.format(self.timestamp_now(), msg))99 print("{0} {1}".format(self.timestamp_now(), msg))
94100
95 def delete_files(self, existing_images_ids):101 def delete_files(self, existing_images_ids):
96 if not existing_images_ids:102 if not existing_images_ids:
97 self.log('WARNING: precautionary halt. '103 self.log("WARNING: precautionary halt. No glance images found. noop.")
98 'No glance images found. noop.')
99 return104 return
100105
101 for dirpath, dirnames, filenames in os.walk(self.DATA_DIR):106 for dirpath, dirnames, filenames in os.walk(self.DATA_DIR):
102 if dirpath != self.DATA_DIR and \107 if dirpath != self.DATA_DIR and len(dirnames) == 0 and len(filenames) == 0:
103 len(dirnames) == 0 and \
104 len(filenames) == 0:
105 os.rmdir(dirpath)108 os.rmdir(dirpath)
106 continue109 continue
107110
108 for filename in filenames:111 for filename in filenames:
109 full_path = os.path.join(dirpath, filename)112 full_path = os.path.join(dirpath, filename)
110 if filename.endswith('.json.tmp'):113 if filename.endswith(".json.tmp"):
111 self.log('WARNING: temporary file skipped. Please check '114 self.log(
112 '{0}'.format(full_path))115 "WARNING: temporary file skipped. Please check "
116 "{0}".format(full_path)
117 )
113 continue118 continue
114 elif filename.endswith('.json') and \119 elif (
115 filename[:-5] in existing_images_ids:120 filename.endswith(".json") and filename[:-5] in existing_images_ids
121 ):
116 continue122 continue
117 else:123 else:
118 self.log('INFO: image not found in glance - deleting '124 self.log(
119 '{0}'.format(full_path))125 "INFO: image not found in glance - deleting "
126 "{0}".format(full_path)
127 )
120 os.remove(full_path)128 os.remove(full_path)
121129
122 def create_lock(self, lockfile):130 def create_lock(self, lockfile):
123 try:131 try:
124 os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)132 os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
125 except OSError:133 except OSError:
126 self.log('ERROR: could not create lockfile {}'.format(lockfile))134 self.log("ERROR: could not create lockfile {}".format(lockfile))
127135
128 def file_locked(self, lockfile='/tmp/glance_sync_master.lock'):136 def file_locked(self, lockfile="/tmp/glance_sync_master.lock"):
129 if os.path.isfile(lockfile):137 if os.path.isfile(lockfile):
130 return True138 return True
131 else:139 else:
132 return False140 return False
133141
134 def release_lock(self, lockfile='/tmp/glance_sync_master.lock'):142 def release_lock(self, lockfile="/tmp/glance_sync_master.lock"):
135 if os.path.isfile(lockfile):143 if os.path.isfile(lockfile):
136 try:144 try:
137 os.remove(lockfile)145 os.remove(lockfile)
138 except OSError as e:146 except OSError as e:
139 self.log(e)147 self.log(e)
140148
141 def set_filelock(self, lockfile='/tmp/glance_sync_master.lock'):149 def set_filelock(self, lockfile="/tmp/glance_sync_master.lock"):
142 if self.file_locked(lockfile):150 if self.file_locked(lockfile):
143 self.log('WARNING: sync already in progress, exiting')151 self.log("WARNING: sync already in progress, exiting")
144 sys.exit(2)152 sys.exit(2)
145153
146 self.create_lock(lockfile)154 self.create_lock(lockfile)
@@ -152,21 +160,20 @@ class ImageSyncMaster:
152 len(image.id) < 2 : DATA_DIR/<image-id>.json160 len(image.id) < 2 : DATA_DIR/<image-id>.json
153 len(image.id) >= 2: DATA_DIR/XX/XXZZZ.json161 len(image.id) >= 2: DATA_DIR/XX/XXZZZ.json
154 """162 """
155 self.log('getting image from database.')163 self.log("getting image from database.")
156 existing_images = self.get_community_images_from_database()164 existing_images = self.get_community_images_from_database()
157 self.log('Extending with data from api.')165 self.log("Extending with data from api.")
158 for image in self.glance.images.list():166 for image in self.glance.images.list():
159 existing_images.add(image.id)167 existing_images.add(image.id)
160168
161 for image_helper in existing_images:169 for image_helper in existing_images:
162 image = self.glance.images.get(image_helper)170 image = self.glance.images.get(image_helper)
163 if len(image['id']) < 2:171 if len(image["id"]) < 2:
164 basename = '{0}.json'.format(image['id'])172 basename = "{0}.json".format(image["id"])
165 else:173 else:
166 basename = '{0}/{1}.json'.format(174 basename = "{0}/{1}.json".format(str(image["id"])[:2], image["id"])
167 str(image['id'])[:2], image['id'])
168 filename = os.path.join(self.DATA_DIR, basename)175 filename = os.path.join(self.DATA_DIR, basename)
169 if not self.is_latest_metadata(filename, image['updated_at']):176 if not self.is_latest_metadata(filename, image["updated_at"]):
170 self.update_metadata(filename, image)177 self.update_metadata(filename, image)
171178
172 return existing_images179 return existing_images
@@ -181,31 +188,32 @@ class ImageSyncMaster:
181 with open(filename) as meta_file:188 with open(filename) as meta_file:
182 try:189 try:
183 data = json.load(meta_file)190 data = json.load(meta_file)
184 local_updated_at = data['updated_at']191 local_updated_at = data["updated_at"]
185 imageid = data['id']192 imageid = data["id"]
186 except Exception as e:193 except Exception as e:
187 self.log('EXCEPTION: {0}'.format(e))194 self.log("EXCEPTION: {0}".format(e))
188 return False195 return False
189196
190 local_dup = dateutil.parser.parse(local_updated_at)197 local_dup = dateutil.parser.parse(local_updated_at)
191 glance_dup = dateutil.parser.parse(glance_updated_at)198 glance_dup = dateutil.parser.parse(glance_updated_at)
192199
193 if local_dup >= glance_dup:200 if local_dup >= glance_dup:
194 self.log('INFO: {0} up to date'.format(imageid))201 self.log("INFO: {0} up to date".format(imageid))
195 return True202 return True
196 else:203 else:
197 self.log('INFO: {0} outdated. Re-creating local '204 self.log(
198 'copy of the metadata'.format(imageid))205 "INFO: {0} outdated. Re-creating local "
206 "copy of the metadata".format(imageid)
207 )
199 else:208 else:
200 self.log('INFO: {0} not found. Creating a local '209 self.log("INFO: {0} not found. Creating a local copy".format(filename))
201 'copy'.format(filename))
202 return False210 return False
203211
204 def update_metadata(self, filename, glance_metadata):212 def update_metadata(self, filename, glance_metadata):
205 """creates or replaces file with image metadata213 """creates or replaces file with image metadata
206 creates subdirectory if it doesn't exist214 creates subdirectory if it doesn't exist
207 """215 """
208 tmp_file = '{0}.tmp'.format(filename)216 tmp_file = "{0}.tmp".format(filename)
209217
210 if not os.path.exists(os.path.dirname(filename)):218 if not os.path.exists(os.path.dirname(filename)):
211 os.mkdir(os.path.dirname(filename), 0o750)219 os.mkdir(os.path.dirname(filename), 0o750)
@@ -216,32 +224,34 @@ class ImageSyncMaster:
216 self.log(e)224 self.log(e)
217 return False225 return False
218226
219 with open(tmp_file, 'w') as f:227 with open(tmp_file, "w") as f:
220 json.dump(glance_metadata, f, indent=4, ensure_ascii=False)228 json.dump(glance_metadata, f, indent=4, ensure_ascii=False)
221229
222 os.rename(tmp_file, filename)230 os.rename(tmp_file, filename)
223 self.log('INFO: update_metadata :: {0}'.format(filename))231 self.log("INFO: update_metadata :: {0}".format(filename))
224 return True232 return True
225233
226 def add_bs_metadata_keys(self, glance_metadata):234 def add_bs_metadata_keys(self, glance_metadata):
227 keys = [k for k in glance_metadata if k.startswith('bs_')]235 keys = [k for k in glance_metadata if k.startswith("bs_")]
228 if 'bs_owner' in keys:236 if "bs_owner" in keys:
229 msg = 'WARNING: bs_owner metadata should not exist (image: ' \237 msg = (
230 '{0}; bs_owner: {1})'.format(glance_metadata.id,238 "WARNING: bs_owner metadata should not exist (image: "
231 glance_metadata.bs_owner)239 "{0}; bs_owner: {1})".format(
240 glance_metadata.id, glance_metadata.bs_owner
241 )
242 )
232 raise BootStackMetadataError(msg)243 raise BootStackMetadataError(msg)
233 elif glance_metadata['owner'] in self.tenants:244 elif glance_metadata["owner"] in self.tenants:
234 glance_metadata['bs_owner'] = self.tenants[245 glance_metadata["bs_owner"] = self.tenants[glance_metadata["owner"]]
235 glance_metadata['owner']]
236 return glance_metadata246 return glance_metadata
237247
238 def main(self):248 def main(self):
239 self.log('starting glance sync')249 self.log("starting glance sync")
240 # updates local metadata files if outdated250 # updates local metadata files if outdated
241 existing_images = self.parse_glance_images()251 existing_images = self.parse_glance_images()
242 # removes local metadata files from deleted images252 # removes local metadata files from deleted images
243 self.delete_files(existing_images)253 self.delete_files(existing_images)
244 self.log('ending glance sync')254 self.log("ending glance sync")
245 self.release_lock()255 self.release_lock()
246256
247 def get_community_images_from_database(self):257 def get_community_images_from_database(self):
@@ -249,34 +259,37 @@ class ImageSyncMaster:
249259
250 db_img_list = list()260 db_img_list = list()
251 con = mysql.connect(261 con = mysql.connect(
252 host=os.environ['OS_MYSQL_HOST'],262 host=os.environ["OS_MYSQL_HOST"],
253 user=os.environ['OS_MYSQL_USER'],263 user=os.environ["OS_MYSQL_USER"],
254 password=os.environ['OS_MYSQL_PASS'],264 password=os.environ["OS_MYSQL_PASS"],
255 database=os.environ['OS_MYSQL_DB'])265 database=os.environ["OS_MYSQL_DB"],
266 )
256 with closing(con.cursor()) as cur:267 with closing(con.cursor()) as cur:
257 sql = 'SELECT id, name FROM images WHERE deleted = 0 AND ' \268 sql = (
258 "visibility = 'community'"269 "SELECT id, name FROM images WHERE deleted = 0 AND "
270 "visibility = 'community'"
271 )
259 cur.execute(sql)272 cur.execute(sql)
260 for (id, name) in cur.fetchall():273 for (id, name) in cur.fetchall():
261 self.log(274 self.log(
262 'Retrieved community image with id [{}] and name [{}] '275 "Retrieved community image with id [{}] and name [{}] "
263 'from database'.format(id, name))276 "from database".format(id, name)
277 )
264 db_img_list.append(id)278 db_img_list.append(id)
265279
266 return set(db_img_list)280 return set(db_img_list)
267281
268282
269if __name__ == '__main__':283if __name__ == "__main__":
270 parser = argparse.ArgumentParser(description='Synchronize glance images '284 parser = argparse.ArgumentParser(description="Synchronize glance images to disk ")
271 'to disk ')285 parser.add_argument("-d", "--datadir", help="directory to write images to")
272 parser.add_argument('-d', '--datadir', help='directory to write images to')
273 args = parser.parse_args()286 args = parser.parse_args()
274287
275 if args.datadir:288 if args.datadir:
276 data_dir = args.datadir289 data_dir = args.datadir
277 else:290 else:
278 parser.print_help()291 parser.print_help()
279 sys.exit('ERROR: please specify an output directory for images')292 sys.exit("ERROR: please specify an output directory for images")
280293
281 master = ImageSyncMaster(data_dir)294 master = ImageSyncMaster(data_dir)
282 master.main()295 master.main()
diff --git a/src/files/glance_sync_slave.py b/src/files/glance_sync_slave.py
index 702b362..daf6dc6 100755
--- a/src/files/glance_sync_slave.py
+++ b/src/files/glance_sync_slave.py
@@ -10,6 +10,7 @@ import json
10import datetime10import datetime
11import dateutil.parser11import dateutil.parser
12import atexit12import atexit
13
13# import re14# import re
14import shlex15import shlex
15import os_client_config16import os_client_config
@@ -22,61 +23,70 @@ class OSProjectNotFound(Exception):
22 """This indicates no sync is possible23 """This indicates no sync is possible
23 (not defaulting to admin project)24 (not defaulting to admin project)
24 """25 """
26
25 pass27 pass
2628
2729
28class ImageSyncSlave:30class ImageSyncSlave:
29 extra_properties = set(['bs_owner'])31 extra_properties = set(["bs_owner"])
30 glance_properties = set(["architecture",32 glance_properties = set(
31 "checksum",33 [
32 "container_format",34 "architecture",
33 "created_at",35 "checksum",
34 "deleted",36 "container_format",
35 "deleted_at",37 "created_at",
36 "direct_url",38 "deleted",
37 "disk_format",39 "deleted_at",
38 "file",40 "direct_url",
39 "id",41 "disk_format",
40 "instance_uuid",42 "file",
41 "kernel_id",43 "id",
42 "locations",44 "instance_uuid",
43 "min_disk",45 "kernel_id",
44 "min_ram",46 "locations",
45 "name",47 "min_disk",
46 "os_distro",48 "min_ram",
47 "os_version",49 "name",
48 "owner",50 "os_distro",
49 "protected",51 "os_version",
50 "ramdisk_id",52 "owner",
51 "schema",53 "protected",
52 "self",54 "ramdisk_id",
53 "size",55 "schema",
54 "status",56 "self",
55 "tags",57 "size",
56 "updated_at",58 "status",
57 "virtual_size",59 "tags",
58 "visibility"])60 "updated_at",
61 "virtual_size",
62 "visibility",
63 ]
64 )
59 # egrep -B2 readOnly glanceclient/v2/image_schema.py | \65 # egrep -B2 readOnly glanceclient/v2/image_schema.py | \
60 # awk '/\{/ {print $1}' | tr -d \":66 # awk '/\{/ {print $1}' | tr -d \":
61 readonly_properties = set(['file',67 readonly_properties = set(
62 'size',68 [
63 'status',69 "file",
64 'self',70 "size",
65 'direct_url',71 "status",
66 'schema',72 "self",
67 'updated_at',73 "direct_url",
68 'locations',74 "schema",
69 'virtual_size',75 "updated_at",
70 'checksum',76 "locations",
71 'created_at'])77 "virtual_size",
78 "checksum",
79 "created_at",
80 ]
81 )
7282
73 def __init__(self, data_dir, source):83 def __init__(self, data_dir, source):
74 self.projects_slave = {}84 self.projects_slave = {}
75 self.DATA_DIR = data_dir85 self.DATA_DIR = data_dir
76 self.SOURCE = source86 self.SOURCE = source
77 self.valid_properties = self.glance_properties.difference(87 self.valid_properties = self.glance_properties.difference(
78 self.readonly_properties.union(88 self.readonly_properties.union(self.extra_properties)
79 self.extra_properties))89 )
80 self.set_filelock()90 self.set_filelock()
81 self.glance_connect_slave()91 self.glance_connect_slave()
82 self.glance_connect_master()92 self.glance_connect_master()
@@ -84,22 +94,25 @@ class ImageSyncSlave:
84 def download_metadata_from_master(self):94 def download_metadata_from_master(self):
85 """rsync metadata files from source to data_dir"""95 """rsync metadata files from source to data_dir"""
8696
87 if not self.SOURCE.endswith('/'):97 if not self.SOURCE.endswith("/"):
88 self.SOURCE += '/'98 self.SOURCE += "/"
89 if not self.DATA_DIR.endswith('/'):99 if not self.DATA_DIR.endswith("/"):
90 self.DATA_DIR += '/'100 self.DATA_DIR += "/"
91101
92 command = '/usr/bin/rsync -az --delete -e ' \102 command = (
93 "'ssh -o StrictHostKeyChecking=no' " \103 "/usr/bin/rsync -az --delete -e "
94 '{0} {1}'.format(self.SOURCE, self.DATA_DIR)104 "'ssh -o StrictHostKeyChecking=no' "
95 proc = subprocess.Popen(shlex.split(command),105 "{0} {1}".format(self.SOURCE, self.DATA_DIR)
96 stdout=subprocess.PIPE,106 )
97 stderr=subprocess.PIPE)107 proc = subprocess.Popen(
108 shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE
109 )
98 (stdout, stderr) = proc.communicate()110 (stdout, stderr) = proc.communicate()
99 if proc.returncode:111 if proc.returncode:
100 self.log('ERROR: problem while getting data from master '112 self.log(
101 '({0})'.format(command))113 "ERROR: problem while getting data from master ({0})".format(command)
102 self.log('ERROR: {0}'.format(stderr))114 )
115 self.log("ERROR: {0}".format(stderr))
103 self.release_lock()116 self.release_lock()
104 sys.exit(2)117 sys.exit(2)
105 else:118 else:
@@ -111,17 +124,22 @@ class ImageSyncSlave:
111124
112 db_img_list = list()125 db_img_list = list()
113 con = mysql.connect(126 con = mysql.connect(
114 host=os.environ['OS_MYSQL_HOST'],127 host=os.environ["OS_MYSQL_HOST"],
115 user=os.environ['OS_MYSQL_USER'],128 user=os.environ["OS_MYSQL_USER"],
116 password=os.environ['OS_MYSQL_PASS'],129 password=os.environ["OS_MYSQL_PASS"],
117 database=os.environ['OS_MYSQL_DB'])130 database=os.environ["OS_MYSQL_DB"],
131 )
118 with closing(con.cursor()) as cur:132 with closing(con.cursor()) as cur:
119 sql = "SELECT id, name FROM images WHERE deleted = 0 AND " \133 sql = (
120 "visibility = 'community'"134 "SELECT id, name FROM images WHERE deleted = 0 AND "
135 "visibility = 'community'"
136 )
121 cur.execute(sql)137 cur.execute(sql)
122 for (id, name) in cur.fetchall():138 for (id, name) in cur.fetchall():
123 self.log('Retrieved community image with id [{}] and name '139 self.log(
124 '[{}] from database'.format(id, name))140 "Retrieved community image with id [{}] and name "
141 "[{}] from database".format(id, name)
142 )
125 db_img_list.append(id)143 db_img_list.append(id)
126144
127 return set(db_img_list)145 return set(db_img_list)
@@ -134,7 +152,7 @@ class ImageSyncSlave:
134152
135 @returns processed (aka. parsed) images153 @returns processed (aka. parsed) images
136 """154 """
137 self.log('getting image list from slave')155 self.log("getting image list from slave")
138 processed_images_ids = set()156 processed_images_ids = set()
139 to_delete_images_ids = set()157 to_delete_images_ids = set()
140 existing_images = self.get_community_images_from_database()158 existing_images = self.get_community_images_from_database()
@@ -143,10 +161,9 @@ class ImageSyncSlave:
143 for image_helper in existing_images:161 for image_helper in existing_images:
144 image = self.glance_slave.images.get(image_helper)162 image = self.glance_slave.images.get(image_helper)
145 if len(image.id) < 2:163 if len(image.id) < 2:
146 basename = '{0}.json'.format(image.id)164 basename = "{0}.json".format(image.id)
147 else:165 else:
148 basename = '{0}/{1}.json'.format(str(image.id)[:2],166 basename = "{0}/{1}.json".format(str(image.id)[:2], image.id)
149 image.id)
150 filename = os.path.join(self.DATA_DIR, basename)167 filename = os.path.join(self.DATA_DIR, basename)
151 if not os.path.isfile(filename):168 if not os.path.isfile(filename):
152 to_delete_images_ids.add(image.id)169 to_delete_images_ids.add(image.id)
@@ -154,14 +171,16 @@ class ImageSyncSlave:
154171
155 metadata_local = self.read_metadata(filename)172 metadata_local = self.read_metadata(filename)
156 if not metadata_local:173 if not metadata_local:
157 self.log('ERROR: read_metadata did not retrieve anything '174 self.log(
158 '({0})'.format(filename))175 "ERROR: read_metadata did not retrieve anything "
176 "({0})".format(filename)
177 )
159 continue178 continue
160179
161 if metadata_local['checksum'] == image.checksum:180 if metadata_local["checksum"] == image.checksum:
162 if not self.is_latest_metadata(metadata_local['id'],181 if not self.is_latest_metadata(
163 metadata_local['updated_at'],182 metadata_local["id"], metadata_local["updated_at"], image.updated_at
164 image.updated_at):183 ):
165 # checksum ok, metadata outdated184 # checksum ok, metadata outdated
166 self.update_metadata(metadata_local, image)185 self.update_metadata(metadata_local, image)
167 processed_images_ids.add(image.id)186 processed_images_ids.add(image.id)
@@ -170,16 +189,17 @@ class ImageSyncSlave:
170 self.upload_to_slave(metadata_local)189 self.upload_to_slave(metadata_local)
171 processed_images_ids.add(image.id)190 processed_images_ids.add(image.id)
172191
173 self.log('DEBUG: images pending to be deleted: '192 self.log(
174 '{0}'.format(to_delete_images_ids))193 "DEBUG: images pending to be deleted: {0}".format(to_delete_images_ids)
194 )
175 self.delete_images_from_slave(to_delete_images_ids)195 self.delete_images_from_slave(to_delete_images_ids)
176 self.log('DEBUG: processed images (to skip while parsing metadata '196 self.log(
177 'files): {0}'.format(processed_images_ids))197 "DEBUG: processed images (to skip while parsing metadata "
198 "files): {0}".format(processed_images_ids)
199 )
178 return processed_images_ids200 return processed_images_ids
179201
180 def is_latest_metadata(self, image_id,202 def is_latest_metadata(self, image_id, master_updated_at, slave_updated_at):
181 master_updated_at,
182 slave_updated_at):
183 """Compares filename content (JSON metadata) and glance service info203 """Compares filename content (JSON metadata) and glance service info
184 @return204 @return
185 True: no need to update205 True: no need to update
@@ -189,70 +209,78 @@ class ImageSyncSlave:
189 slave_dup = dateutil.parser.parse(slave_updated_at)209 slave_dup = dateutil.parser.parse(slave_updated_at)
190210
191 if master_dup <= slave_dup:211 if master_dup <= slave_dup:
192 self.log('INFO: is_latest_metadata :: {0} up to '212 self.log("INFO: is_latest_metadata :: {0} up to date".format(image_id))
193 'date'.format(image_id))
194 return True213 return True
195 else:214 else:
196 self.log('INFO: is_latest_metadata :: {0} outdated. Needs '215 self.log(
197 'update_metadata.'.format(image_id))216 "INFO: is_latest_metadata :: {0} outdated. Needs "
217 "update_metadata.".format(image_id)
218 )
198 return False219 return False
199220
200 def upload_to_slave(self, metadata_local): # noqa: C901 is too complex (12)221 def upload_to_slave(self, metadata_local): # noqa: C901 is too complex (12)
201 """upload image to glance slave service222 """upload image to glance slave service
202 """223 """
203 tmp_image_basename = '{0}.img'.format(metadata_local['id'])224 tmp_image_basename = "{0}.img".format(metadata_local["id"])
204 tmp_image = os.path.join(self.DATA_DIR, tmp_image_basename)225 tmp_image = os.path.join(self.DATA_DIR, tmp_image_basename)
205 try:226 try:
206 clean_metadata, removed_props = self.mangle_metadata(metadata_local)227 clean_metadata, removed_props = self.mangle_metadata(metadata_local)
207 except OSProjectNotFound as e:228 except OSProjectNotFound as e:
208 self.log('EXCEPTION: upload_to_slave :: image-id {0} :: '229 self.log(
209 'problem uploading data to glance '230 "EXCEPTION: upload_to_slave :: image-id {0} :: "
210 'slave (image could not be removed) :: '231 "problem uploading data to glance "
211 '{1}'.format(metadata_local['id'], e))232 "slave (image could not be removed) :: "
233 "{1}".format(metadata_local["id"], e)
234 )
212 return False235 return False
213236
214 for k in removed_props:237 for k in removed_props:
215 if k in clean_metadata:238 if k in clean_metadata:
216 del clean_metadata[k]239 del clean_metadata[k]
217240
218 self.log('INFO: creating image {0}'.format(clean_metadata['id']))241 self.log("INFO: creating image {0}".format(clean_metadata["id"]))
219 try:242 try:
220 self.glance_slave.images.create(**clean_metadata)243 self.glance_slave.images.create(**clean_metadata)
221 self.log('DEBUG: create image: {0}'.format(clean_metadata))244 self.log("DEBUG: create image: {0}".format(clean_metadata))
222 except Exception as e: # TODO narrow this exception down245 except Exception as e: # TODO narrow this exception down
223 self.log('EXCEPTION: upload_to_slave :: {0}'.format(e))246 self.log("EXCEPTION: upload_to_slave :: {0}".format(e))
224 try:247 try:
225 # update metadata248 # update metadata
226 self.glance_slave.images.update(clean_metadata['id'],249 self.glance_slave.images.update(
227 remove_props=removed_props,250 clean_metadata["id"], remove_props=removed_props, **clean_metadata
228 **clean_metadata)251 )
229 self.log('DEBUG: update_to_slave :: update metadata '252 self.log(
230 '{0}'.format(clean_metadata))253 "DEBUG: update_to_slave :: update metadata "
254 "{0}".format(clean_metadata)
255 )
231 except Exception as e:256 except Exception as e:
232 if "HTTPNotFound" not in e:257 if "HTTPNotFound" not in e:
233 self.log('ERROR: update_to_slave (both image '258 self.log(
234 'create/update failed :: {0} - this can '259 "ERROR: update_to_slave (both image "
235 'happen if the image was deleted through '260 "create/update failed :: {0} - this can "
236 'the API but still exists in the glance '261 "happen if the image was deleted through "
237 'database :: {1}'262 "the API but still exists in the glance "
238 .format(clean_metadata['id'], e))263 "database :: {1}".format(clean_metadata["id"], e)
264 )
239 return False265 return False
240266
241 self.log('ERROR: {0} {1} is likely deleted'.format(267 self.log(
242 clean_metadata['id'], e))268 "ERROR: {0} {1} is likely deleted".format(clean_metadata["id"], e)
269 )
243270
244 try:271 try:
245 # Upload.272 # Upload.
246 self.glance_slave.images.upload(clean_metadata['id'],273 self.glance_slave.images.upload(clean_metadata["id"], open(tmp_image, "rb"))
247 open(tmp_image, 'rb'))
248 os.remove(tmp_image)274 os.remove(tmp_image)
249 self.log('DEBUG: update_to_slave :: upload {0}'.format(tmp_image))275 self.log("DEBUG: update_to_slave :: upload {0}".format(tmp_image))
250 except Exception as e:276 except Exception as e:
251 os.remove(tmp_image)277 os.remove(tmp_image)
252 self.log('ERROR: upload_to_slave :: image-id {0} :: '278 self.log(
253 'problem uploading data to glance '279 "ERROR: upload_to_slave :: image-id {0} :: "
254 'slave (image could not be removed) :: '280 "problem uploading data to glance "
255 '{1}'.format(clean_metadata['id'], e))281 "slave (image could not be removed) :: "
282 "{1}".format(clean_metadata["id"], e)
283 )
256 return False284 return False
257285
258 def download_from_master(self, metadata_local): # noqa: C901 is too complex (12)286 def download_from_master(self, metadata_local): # noqa: C901 is too complex (12)
@@ -261,45 +289,54 @@ class ImageSyncSlave:
261 @return True: downloaded or already on local storage289 @return True: downloaded or already on local storage
262 @return False: error290 @return False: error
263 """291 """
264 tmp_image_basename = '{0}.img'.format(metadata_local['id'])292 tmp_image_basename = "{0}.img".format(metadata_local["id"])
265 tmp_image = os.path.join(self.DATA_DIR, tmp_image_basename)293 tmp_image = os.path.join(self.DATA_DIR, tmp_image_basename)
266 if os.path.isfile(tmp_image):294 if os.path.isfile(tmp_image):
267 if self.check_md5(metadata_local['checksum'], tmp_image):295 if self.check_md5(metadata_local["checksum"], tmp_image):
268 return True296 return True
269297
270 try:298 try:
271 os.remove(tmp_image)299 os.remove(tmp_image)
272 except Exception as e:300 except Exception as e:
273 self.log('ERROR: download_from_master :: {0}'.format(e))301 self.log("ERROR: download_from_master :: {0}".format(e))
274 return False302 return False
275 downloaded = False303 downloaded = False
276 retries = 3304 retries = 3
277 for i in range(0, retries):305 for i in range(0, retries):
278 try:306 try:
279 bin_image = self.glance_master.images.data(307 bin_image = self.glance_master.images.data(
280 image_id=metadata_local['id'])308 image_id=metadata_local["id"]
309 )
281310
282 hash_md5 = hashlib.md5()311 hash_md5 = hashlib.md5()
283 with open(tmp_image, 'wb') as fd:312 with open(tmp_image, "wb") as fd:
284 for chunk in bin_image:313 for chunk in bin_image:
285 fd.write(chunk)314 fd.write(chunk)
286 hash_md5.update(chunk)315 hash_md5.update(chunk)
287 bin_image_checksum = hash_md5.hexdigest()316 bin_image_checksum = hash_md5.hexdigest()
288 if metadata_local['checksum'] == bin_image_checksum:317 if metadata_local["checksum"] == bin_image_checksum:
289 downloaded = True318 downloaded = True
290 self.log('INFO: download_from_master ({0} - {1}):: '319 self.log(
291 'checksum OK'.format(metadata_local['id'],320 "INFO: download_from_master ({0} - {1}):: "
292 metadata_local['checksum']))321 "checksum OK".format(
322 metadata_local["id"], metadata_local["checksum"]
323 )
324 )
293 break325 break
294 elif os.path.exists(tmp_image):326 elif os.path.exists(tmp_image):
295 self.log('INFO: download_from_master ({0}/{1}; {2}):: '327 self.log(
296 'invalid checksum '328 "INFO: download_from_master ({0}/{1}; {2}):: "
297 '{3}'.format(metadata_local['id'], i, retries,329 "invalid checksum "
298 bin_image_checksum))330 "{3}".format(
331 metadata_local["id"], i, retries, bin_image_checksum
332 )
333 )
299 os.remove(tmp_image)334 os.remove(tmp_image)
300 except Exception as e:335 except Exception as e:
301 self.log('EXCEPTION: download_from_master ({0}/{1}; {2}):: '336 self.log(
302 '{3}'.format(i, retries, metadata_local['id'], e))337 "EXCEPTION: download_from_master ({0}/{1}; {2}):: "
338 "{3}".format(i, retries, metadata_local["id"], e)
339 )
303 if os.path.exists(tmp_image):340 if os.path.exists(tmp_image):
304 os.remove(tmp_image)341 os.remove(tmp_image)
305342
@@ -310,86 +347,90 @@ class ImageSyncSlave:
310 deletes images not found in local storage347 deletes images not found in local storage
311 """348 """
312 if not to_delete_images_ids:349 if not to_delete_images_ids:
313 self.log('WARNING: precautionary halt. No glance images found '350 self.log(
314 'to be deleted. noop.')351 "WARNING: precautionary halt. No glance images found "
352 "to be deleted. noop."
353 )
315 return354 return
316355
317 for image_id in to_delete_images_ids:356 for image_id in to_delete_images_ids:
318 self.log('INFO: removing image {0}'.format(image_id))357 self.log("INFO: removing image {0}".format(image_id))
319 try:358 try:
320 self.glance_slave.images.delete(image_id)359 self.glance_slave.images.delete(image_id)
321 self.log('DEBUG: image {0} removed'.format(image_id))360 self.log("DEBUG: image {0} removed".format(image_id))
322 except Exception as e: # TODO narrow the exception down361 except Exception as e: # TODO narrow the exception down
323 self.log('ERROR: could not delete {0} :: '362 self.log("ERROR: could not delete {0} :: {1}".format(image_id, e))
324 '{1}'.format(image_id, e))
325363
326 def create_missing_slave_images(self, processed_images_ids):364 def create_missing_slave_images(self, processed_images_ids):
327365
328 for dirpath, dirnames, filenames in os.walk(self.DATA_DIR):366 for dirpath, dirnames, filenames in os.walk(self.DATA_DIR):
329 if dirpath != self.DATA_DIR and \367 if dirpath != self.DATA_DIR and len(dirnames) == 0 and len(filenames) == 0:
330 len(dirnames) == 0 and \
331 len(filenames) == 0:
332 os.rmdir(dirpath)368 os.rmdir(dirpath)
333 continue369 continue
334370
335 for filename in filenames:371 for filename in filenames:
336 full_path = os.path.join(dirpath, filename)372 full_path = os.path.join(dirpath, filename)
337 if filename.endswith('.json'):373 if filename.endswith(".json"):
338 image_id = filename[:-5]374 image_id = filename[:-5]
339 if image_id in processed_images_ids:375 if image_id in processed_images_ids:
340 continue376 continue
341377
342 metadata_local = self.read_metadata(full_path)378 metadata_local = self.read_metadata(full_path)
343 if not metadata_local:379 if not metadata_local:
344 self.log('ERROR: read_metadata did not '380 self.log(
345 'retrieve anything '381 "ERROR: read_metadata did not "
346 '({0})'.format(full_path))382 "retrieve anything "
383 "({0})".format(full_path)
384 )
347 continue385 continue
348386
349 slave_project_id = self.project_mapping(metadata_local)387 slave_project_id = self.project_mapping(metadata_local)
350 if not slave_project_id:388 if not slave_project_id:
351 self.log('DEBUG: could not map image into any '389 self.log(
352 'slave project :: {0}'.format(metadata_local))390 "DEBUG: could not map image into any "
391 "slave project :: {0}".format(metadata_local)
392 )
353 continue393 continue
354394
355 metadata_local['owner'] = slave_project_id395 metadata_local["owner"] = slave_project_id
356 if self.download_from_master(metadata_local):396 if self.download_from_master(metadata_local):
357 self.upload_to_slave(metadata_local)397 self.upload_to_slave(metadata_local)
358 else:398 else:
359 self.log('ERROR: image {0} could not be downloaded '399 self.log(
360 'from master'.format(metadata_local['id']))400 "ERROR: image {0} could not be downloaded "
401 "from master".format(metadata_local["id"])
402 )
361403
362 def project_mapping(self, metadata_local):404 def project_mapping(self, metadata_local):
363 """can master/slave projects be mapped, no matter project_ids are not405 """can master/slave projects be mapped, no matter project_ids are not
364 the same?406 the same?
365 """407 """
366 # master/slave match can't be done (no project_id)408 # master/slave match can't be done (no project_id)
367 if 'owner' not in metadata_local:409 if "owner" not in metadata_local:
368 return False410 return False
369411
370 # master/slave project_ids match412 # master/slave project_ids match
371 if metadata_local['owner'] in self.projects_slave:413 if metadata_local["owner"] in self.projects_slave:
372 return metadata_local['owner']414 return metadata_local["owner"]
373415
374 # no extra project_name passed -- can't check match416 # no extra project_name passed -- can't check match
375 if 'bs_owner' not in metadata_local:417 if "bs_owner" not in metadata_local:
376 return False418 return False
377419
378 master_project_name = metadata_local['bs_owner']420 master_project_name = metadata_local["bs_owner"]
379421
380 # XXX(aluria): no image on slave service422 # XXX(aluria): no image on slave service
381 # XXX(aluria): look for similar project on slave423 # XXX(aluria): look for similar project on slave
382 for slave_project_id, slave_project_name in (424 for slave_project_id, slave_project_name in self.projects_slave.items():
383 self.projects_slave.items()):425 slave_to_master = slave_project_name.replace(
384 slave_to_master = slave_project_name.replace(self.REGION_SLAVE,426 self.REGION_SLAVE, self.REGION_MASTER
385 self.REGION_MASTER)427 )
386 # XXX(aluria): pitfall, if on master service there are428 # XXX(aluria): pitfall, if on master service there are
387 # XXX(aluria): 2 projects:429 # XXX(aluria): 2 projects:
388 # XXX(auria): REGION_SLAVE-restofprojectname430 # XXX(auria): REGION_SLAVE-restofprojectname
389 # XXX(auria): REGION_MASTER-restofprojectname431 # XXX(auria): REGION_MASTER-restofprojectname
390 # XXX(auria): first found gets image assigned432 # XXX(auria): first found gets image assigned
391 if master_project_name in (slave_project_name,433 if master_project_name in (slave_project_name, slave_to_master):
392 slave_to_master):
393 return slave_project_id434 return slave_project_id
394 return False435 return False
395436
@@ -413,49 +454,47 @@ class ImageSyncSlave:
413 data = json.load(meta_file)454 data = json.load(meta_file)
414 return data455 return data
415 except Exception as e:456 except Exception as e:
416 self.log('EXCEPTION: {0}'.format(e))457 self.log("EXCEPTION: {0}".format(e))
417 return False458 return False
418 else:459 else:
419 self.log('INFO: {0} not found.'.format(metadata_file))460 self.log("INFO: {0} not found.".format(metadata_file))
420 return False461 return False
421462
422 def glance_connect_slave(self):463 def glance_connect_slave(self):
423 try:464 try:
424 self.keystone = os_client_config.session_client(465 self.keystone = os_client_config.session_client(
425 'identity',466 "identity", cloud="envvars",
426 cloud='envvars',
427 )
428 self.glance_slave = os_client_config.make_client(
429 'image',
430 cloud='envvars',
431 )467 )
468 self.glance_slave = os_client_config.make_client("image", cloud="envvars")
432 except Exception as e:469 except Exception as e:
433 self.log('EXCEPTION: {0}'.format(e))470 self.log("EXCEPTION: {0}".format(e))
434 self.log('ERROR: unable to load environment variables, please '471 self.log(
435 'source novarc')472 "ERROR: unable to load environment variables, please source novarc"
473 )
436 self.release_lock()474 self.release_lock()
437 sys.exit(2)475 sys.exit(2)
438 if not self.projects_slave:476 if not self.projects_slave:
439 self.projects_slave = dict(477 self.projects_slave = dict(
440 [(tenant['id'], tenant['name'])478 [
441 for tenant in self.keystone.get('/projects').json()['projects']479 (tenant["id"], tenant["name"])
442 if tenant['enabled']]480 for tenant in self.keystone.get("/projects").json()["projects"]
481 if tenant["enabled"]
482 ]
443 )483 )
444 self.REGION_SLAVE = os.environ['OS_REGION_NAME'].upper()484 self.REGION_SLAVE = os.environ["OS_REGION_NAME"].upper()
445 return self.glance_slave485 return self.glance_slave
446486
447 def glance_connect_master(self):487 def glance_connect_master(self):
448 try:488 try:
449 self.glance_master = os_client_config.make_client(489 self.glance_master = os_client_config.make_client("image", cloud="master")
450 'image',490 self.REGION_MASTER = os.environ["OS_MASTER_REGION"]
451 cloud='master',
452 )
453 self.REGION_MASTER = os.environ['OS_MASTER_REGION']
454 except Exception as e:491 except Exception as e:
455 self.log('EXCEPTION: {0}'.format(e))492 self.log("EXCEPTION: {0}".format(e))
456 self.log('ERROR: unable to load master cloud environment, '493 self.log(
457 'please check master_creds settings and '494 "ERROR: unable to load master cloud environment, "
458 '/etc/openstack/clouds.yaml')495 "please check master_creds settings and "
496 "/etc/openstack/clouds.yaml"
497 )
459 self.release_lock()498 self.release_lock()
460 sys.exit(2)499 sys.exit(2)
461 return self.glance_master500 return self.glance_master
@@ -464,7 +503,7 @@ class ImageSyncSlave:
464 return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")503 return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
465504
466 def log(self, msg):505 def log(self, msg):
467 print('{0} {1}'.format(self.timestamp_now(), msg))506 print("{0} {1}".format(self.timestamp_now(), msg))
468507
469 def mangle_metadata(self, metadata_local, metadata_slave=None):508 def mangle_metadata(self, metadata_local, metadata_slave=None):
470 """Maps projects in MASTER region with projects in SLAVE region509 """Maps projects in MASTER region with projects in SLAVE region
@@ -519,122 +558,132 @@ class ImageSyncSlave:
519 'base_image_ref',558 'base_image_ref',
520 'owner_id']559 'owner_id']
521 """560 """
522 if 'owner' not in metadata_local:561 if "owner" not in metadata_local:
523 raise (OSProjectNotFound, 'no owner :: {0}'.format(metadata_local))562 raise (OSProjectNotFound, "no owner :: {0}".format(metadata_local))
524563
525 if metadata_local['owner'] not in self.projects_slave:564 if metadata_local["owner"] not in self.projects_slave:
526 if 'bs_owner' in metadata_local:565 if "bs_owner" in metadata_local:
527 master_project_name = metadata_local['bs_owner']566 master_project_name = metadata_local["bs_owner"]
528 else:567 else:
529 raise (OSProjectNotFound, 'no bs_owner :: '568 raise (
530 '{0}'.format(metadata_local))569 OSProjectNotFound,
570 "no bs_owner :: {0}".format(metadata_local),
571 )
531572
532 # XXX(aluria): image does not exist on slave service573 # XXX(aluria): image does not exist on slave service
533 if not metadata_slave:574 if not metadata_slave:
534 slave_project_id = self.project_mapping(metadata_local)575 slave_project_id = self.project_mapping(metadata_local)
535 if not slave_project_id:576 if not slave_project_id:
536 raise (OSProjectNotFound, 'no project_mapping :: '577 raise (
537 '{0}'.format(metadata_local))578 OSProjectNotFound,
538 elif metadata_local['owner'] != slave_project_id:579 "no project_mapping :: {0}".format(metadata_local),
539 metadata_local['owner'] = slave_project_id580 )
581 elif metadata_local["owner"] != slave_project_id:
582 metadata_local["owner"] = slave_project_id
540 # XXX(aluria): image exists on slave service583 # XXX(aluria): image exists on slave service
541 # XXX(aluria): keep all metadata and mangle project_id (owner)584 # XXX(aluria): keep all metadata and mangle project_id (owner)
542 else:585 else:
543 # ie. admin, services, SLAVE-CENTRAL586 # ie. admin, services, SLAVE-CENTRAL
544 slave_project_name = self.projects_slave[metadata_slave['owner']]587 slave_project_name = self.projects_slave[metadata_slave["owner"]]
545 # ie. admin, services, MASTER-CENTRAL588 # ie. admin, services, MASTER-CENTRAL
546 slave_to_master = \589 slave_to_master = slave_project_name.replace(
547 slave_project_name.replace(self.REGION_SLAVE,590 self.REGION_SLAVE, self.REGION_MASTER
548 self.REGION_MASTER)591 )
549 # ie. admin, services, MASTER-CENTRAL592 # ie. admin, services, MASTER-CENTRAL
550 if master_project_name in (slave_project_name,593 if master_project_name in (slave_project_name, slave_to_master):
551 slave_to_master):594 metadata_local["owner"] = metadata_slave["owner"]
552 metadata_local['owner'] = metadata_slave['owner']
553 else:595 else:
554 raise (OSProjectNotFound, 'project not found: '596 raise (
555 '{0}'.format(metadata_local))597 OSProjectNotFound,
598 "project not found: {0}".format(metadata_local),
599 )
556600
557 removed_props = [k for k in metadata_local.keys() if k not in601 removed_props = [
558 self.valid_properties]602 k for k in metadata_local.keys() if k not in self.valid_properties
603 ]
559 return (metadata_local, removed_props)604 return (metadata_local, removed_props)
560605
561 def update_metadata(self, metadata_local, metadata_slave):606 def update_metadata(self, metadata_local, metadata_slave):
562 self.log('INFO: image-id {0}: updating '607 self.log("INFO: image-id {0}: updating metadata".format(metadata_local["id"]))
563 'metadata'.format(metadata_local['id']))
564608
565 metadata, removed_props = self.mangle_metadata(metadata_local,609 metadata, removed_props = self.mangle_metadata(metadata_local, metadata_slave)
566 metadata_slave)
567 for k in removed_props:610 for k in removed_props:
568 if k in metadata:611 if k in metadata:
569 del metadata[k]612 del metadata[k]
570613
571 try:614 try:
572 self.glance_slave.images.update(metadata['id'],615 self.glance_slave.images.update(metadata["id"], **metadata)
573 **metadata)
574 except Exception as e:616 except Exception as e:
575 self.log('EXCEPTION: update_metadata :: {0} - '617 self.log(
576 '{1}'.format(metadata['id'], e))618 "EXCEPTION: update_metadata :: {0} - {1}".format(metadata["id"], e)
619 )
577 raise e620 raise e
578621
579 def create_lock(self, lockfile):622 def create_lock(self, lockfile):
580 try:623 try:
581 with open(lockfile, 'w') as lock:624 with open(lockfile, "w") as lock:
582 lock.write(str(os.getpid()))625 lock.write(str(os.getpid()))
583 except OSError:626 except OSError:
584 self.log('ERROR: could not create lockfile {0}'.format(lockfile))627 self.log("ERROR: could not create lockfile {0}".format(lockfile))
585628
586 def file_locked(self, lockfile='/tmp/glance_sync_slave.lock'):629 def file_locked(self, lockfile="/tmp/glance_sync_slave.lock"):
587 if os.path.isfile(lockfile):630 if os.path.isfile(lockfile):
588 return True631 return True
589 else:632 else:
590 return False633 return False
591634
592 def release_lock(self, lockfile='/tmp/glance_sync_slave.lock'):635 def release_lock(self, lockfile="/tmp/glance_sync_slave.lock"):
593 if os.path.isfile(lockfile):636 if os.path.isfile(lockfile):
594 try:637 try:
595 os.remove(lockfile)638 os.remove(lockfile)
596 except OSError as e:639 except OSError as e:
597 self.log(e)640 self.log(e)
598641
599 def set_filelock(self, lockfile='/tmp/glance_sync_slave.lock'):642 def set_filelock(self, lockfile="/tmp/glance_sync_slave.lock"):
600 if self.file_locked(lockfile):643 if self.file_locked(lockfile):
601 self.log('WARNING: sync already in progress, exiting')644 self.log("WARNING: sync already in progress, exiting")
602 sys.exit(2)645 sys.exit(2)
603646
604 self.create_lock(lockfile)647 self.create_lock(lockfile)
605 atexit.register(self.release_lock)648 atexit.register(self.release_lock)
606649
607 def main(self):650 def main(self):
608 self.log('starting glance sync')651 self.log("starting glance sync")
609 self.log('getting metadata from master')652 self.log("getting metadata from master")
610 self.download_metadata_from_master()653 self.download_metadata_from_master()
611 processed_images_ids = self.parse_glance_slave_images()654 processed_images_ids = self.parse_glance_slave_images()
612 self.create_missing_slave_images(processed_images_ids)655 self.create_missing_slave_images(processed_images_ids)
613 self.log('ending glance image sync slave run')656 self.log("ending glance image sync slave run")
614 self.release_lock()657 self.release_lock()
615658
616659
617if __name__ == '__main__':660if __name__ == "__main__":
618 parser = argparse.ArgumentParser(description='Synchronize remote images '661 parser = argparse.ArgumentParser(
619 'metadata to disk and import into glance')662 description="Synchronize remote images "
663 "metadata to disk and import into glance"
664 )
620 parser.add_argument("-d", "--datadir", help="directory to write images to")665 parser.add_argument("-d", "--datadir", help="directory to write images to")
621 parser.add_argument("-s", "--source", help="full path to master rsync "666 parser.add_argument(
622 "source. Format: "667 "-s",
623 "<user>@<hostname>:<port>/"668 "--source",
624 "<directory>")669 help="full path to master rsync "
670 "source. Format: "
671 "<user>@<hostname>:<port>/"
672 "<directory>",
673 )
625 args = parser.parse_args()674 args = parser.parse_args()
626675
627 if args.datadir:676 if args.datadir:
628 data_dir = args.datadir677 data_dir = args.datadir
629 else:678 else:
630 parser.print_help()679 parser.print_help()
631 sys.exit('ERROR: please specify an output directory for images')680 sys.exit("ERROR: please specify an output directory for images")
632681
633 if args.source:682 if args.source:
634 source = args.source683 source = args.source
635 else:684 else:
636 parser.print_help()685 parser.print_help()
637 sys.exit('ERROR: please specify an image source to sync from')686 sys.exit("ERROR: please specify an image source to sync from")
638687
639 slave = ImageSyncSlave(data_dir, source)688 slave = ImageSyncSlave(data_dir, source)
640 slave.main()689 slave.main()
diff --git a/src/reactive/glance_sync.py b/src/reactive/glance_sync.py
index bd80f43..dff83c6 100644
--- a/src/reactive/glance_sync.py
+++ b/src/reactive/glance_sync.py
@@ -15,59 +15,53 @@ from charmhelpers.contrib.openstack.utils import config_flags_parser
15from charms.reactive import hook, clear_flag, when, when_any15from charms.reactive import hook, clear_flag, when, when_any
1616
1717
18@hook('install')18@hook("install")
19def install_glance_sync():19def install_glance_sync():
20 """Install glance-sync charm."""20 """Install glance-sync charm."""
21 hookenv.status_set('maintenance', 'Installing')21 hookenv.status_set("maintenance", "Installing")
22 configure_config_dir()22 configure_config_dir()
23 configure_log_dir()23 configure_log_dir()
24 configure_script_dir()24 configure_script_dir()
25 configure_sync_mode()25 configure_sync_mode()
2626
27 homedir = os.path.expanduser('~ubuntu')27 homedir = os.path.expanduser("~ubuntu")
28 ssh_identity = '{}/.ssh/id_rsa'.format(homedir)28 ssh_identity = "{}/.ssh/id_rsa".format(homedir)
29 if not os.path.exists(ssh_identity):29 if not os.path.exists(ssh_identity):
30 command = ['ssh-keygen', '-t', 'rsa', '-N', '',30 command = ["ssh-keygen", "-t", "rsa", "-N", "", "-f", ssh_identity]
31 '-f', ssh_identity]31 proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
32 proc = subprocess.Popen(command,
33 stdout=subprocess.PIPE,
34 stderr=subprocess.PIPE)
35 (stdout, stderr) = proc.communicate()32 (stdout, stderr) = proc.communicate()
36 if proc.returncode:33 if proc.returncode:
37 print("ERROR: problem generating ssh key '{}':"34 print("ERROR: problem generating ssh key '{}':".format(command))
38 .format(command))
39 print(stderr)35 print(stderr)
40 os.chown(ssh_identity,36 os.chown(
41 pwd.getpwnam('ubuntu').pw_uid,37 ssh_identity, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid
42 grp.getgrnam('ubuntu').gr_gid)38 )
4339
44 hookenv.status_set('active', 'Unit is ready')40 hookenv.status_set("active", "Unit is ready")
4541
4642
47@when('config.changed.master_mode')43@when("config.changed.master_mode")
48def configure_sync_mode():44def configure_sync_mode():
49 """Configure glance-sync charm to be either master or slave."""45 """Configure glance-sync charm to be either master or slave."""
50 master_enabled = hookenv.config('master_mode')46 master_enabled = hookenv.config("master_mode")
51 hookenv.log('configuring mode')47 hookenv.log("configuring mode")
52 if master_enabled:48 if master_enabled:
53 hookenv.status_set('maintenance',49 hookenv.status_set("maintenance", "Configuring master")
54 'Configuring master')50 hookenv.log("configuring unit as master")
55 hookenv.log('configuring unit as master')
56 perform_slave_cleanup()51 perform_slave_cleanup()
57 install_master_sync_script()52 install_master_sync_script()
58 hookenv.log('opening TCP port 22')53 hookenv.log("opening TCP port 22")
59 open_port(22, protocol='TCP')54 open_port(22, protocol="TCP")
60 else:55 else:
61 hookenv.status_set('maintenance',56 hookenv.status_set("maintenance", "Configuring slave")
62 'Configuring slave')57 hookenv.log("configuring unit as slave")
63 hookenv.log('configuring unit as slave')
64 perform_master_cleanup()58 perform_master_cleanup()
65 install_slave_sync_script()59 install_slave_sync_script()
6660
67 install_db_cleanup_script()61 install_db_cleanup_script()
68 configure_cron()62 configure_cron()
69 configure_data_dir()63 configure_data_dir()
70 hookenv.status_set('active', 'Unit is ready')64 hookenv.status_set("active", "Unit is ready")
7165
7266
73def perform_slave_cleanup():67def perform_slave_cleanup():
@@ -75,29 +69,29 @@ def perform_slave_cleanup():
75 Cleanup glance-sync slave files, once the charm is set69 Cleanup glance-sync slave files, once the charm is set
76 to be the master, after being a slave.70 to be the master, after being a slave.
77 """71 """
78 data_dir = hookenv.config('data_dir')72 data_dir = hookenv.config("data_dir")
79 if os.path.exists(data_dir):73 if os.path.exists(data_dir):
80 shutil.rmtree(data_dir, ignore_errors=True)74 shutil.rmtree(data_dir, ignore_errors=True)
8175
82 script_dir = hookenv.config('script_dir')76 script_dir = hookenv.config("script_dir")
83 if os.path.exists(script_dir):77 if os.path.exists(script_dir):
84 shutil.rmtree(script_dir, ignore_errors=True)78 shutil.rmtree(script_dir, ignore_errors=True)
8579
86 config_dir = hookenv.config('config_dir')80 config_dir = hookenv.config("config_dir")
87 novarc_file = os.path.join(config_dir, 'novarc')81 novarc_file = os.path.join(config_dir, "novarc")
88 if os.path.isfile(novarc_file):82 if os.path.isfile(novarc_file):
89 os.remove(novarc_file)83 os.remove(novarc_file)
9084
91 clouds_yaml_dir = '/etc/openstack'85 clouds_yaml_dir = "/etc/openstack"
92 if os.path.exists(clouds_yaml_dir):86 if os.path.exists(clouds_yaml_dir):
93 shutil.rmtree(clouds_yaml_dir, ignore_errors=True)87 shutil.rmtree(clouds_yaml_dir, ignore_errors=True)
9488
95 cron_file = '/etc/cron.d/glance_sync_slave'89 cron_file = "/etc/cron.d/glance_sync_slave"
96 if os.path.isfile(cron_file):90 if os.path.isfile(cron_file):
97 os.remove(cron_file)91 os.remove(cron_file)
98 clear_flag('cron.configured')92 clear_flag("cron.configured")
9993
100 clear_flag('slave.configured')94 clear_flag("slave.configured")
10195
10296
103def perform_master_cleanup():97def perform_master_cleanup():
@@ -105,168 +99,162 @@ def perform_master_cleanup():
105 Cleanup glance-sync master files, once the charm is set99 Cleanup glance-sync master files, once the charm is set
106 to be a slave, after being the master.100 to be a slave, after being the master.
107 """101 """
108 data_dir = hookenv.config('data_dir')102 data_dir = hookenv.config("data_dir")
109 if os.path.exists(data_dir):103 if os.path.exists(data_dir):
110 shutil.rmtree(data_dir, ignore_errors=True)104 shutil.rmtree(data_dir, ignore_errors=True)
111105
112 script_dir = hookenv.config('script_dir')106 script_dir = hookenv.config("script_dir")
113 if os.path.exists(script_dir):107 if os.path.exists(script_dir):
114 shutil.rmtree(script_dir, ignore_errors=True)108 shutil.rmtree(script_dir, ignore_errors=True)
115109
116 config_dir = hookenv.config('config_dir')110 config_dir = hookenv.config("config_dir")
117 novarc_file = os.path.join(config_dir, 'novarc')111 novarc_file = os.path.join(config_dir, "novarc")
118 if os.path.isfile(novarc_file):112 if os.path.isfile(novarc_file):
119 os.remove(novarc_file)113 os.remove(novarc_file)
120114
121 cron_file = '/etc/cron.d/glance_sync_master'115 cron_file = "/etc/cron.d/glance_sync_master"
122 if os.path.isfile(cron_file):116 if os.path.isfile(cron_file):
123 os.remove(cron_file)117 os.remove(cron_file)
124 clear_flag('cron.configured')118 clear_flag("cron.configured")
125119
126 clear_flag('master.configured')120 clear_flag("master.configured")
127121
128122
129@hook('upgrade-charm')123@hook("upgrade-charm")
130def upgrade_glance_sync():124def upgrade_glance_sync():
131 """Perform charm upgrade."""125 """Perform charm upgrade."""
132 install_glance_sync()126 install_glance_sync()
133127
134128
135@when('config.changed.config_dir')129@when("config.changed.config_dir")
136def configure_config_dir():130def configure_config_dir():
137 """Configure 'config_dir' directory, and configure cron."""131 """Configure 'config_dir' directory, and configure cron."""
138 hookenv.status_set('maintenance', 'Configuring')132 hookenv.status_set("maintenance", "Configuring")
139 config_dir = hookenv.config('config_dir')133 config_dir = hookenv.config("config_dir")
140 if not os.path.exists(config_dir):134 if not os.path.exists(config_dir):
141 os.makedirs(config_dir)135 os.makedirs(config_dir)
142 os.chown(config_dir,136 os.chown(
143 pwd.getpwnam('ubuntu').pw_uid,137 config_dir, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid
144 grp.getgrnam('ubuntu').gr_gid)138 )
145139
146 configure_cron()140 configure_cron()
147 hookenv.status_set('active', 'Unit is ready')141 hookenv.status_set("active", "Unit is ready")
148142
149143
150@when('config.changed.data_dir')144@when("config.changed.data_dir")
151def configure_data_dir():145def configure_data_dir():
152 """Configure 'data_dir' directory, and configure cron."""146 """Configure 'data_dir' directory, and configure cron."""
153 hookenv.status_set('maintenance', 'Configuring')147 hookenv.status_set("maintenance", "Configuring")
154 data_dir = hookenv.config('data_dir')148 data_dir = hookenv.config("data_dir")
155 if not os.path.exists(data_dir):149 if not os.path.exists(data_dir):
156 os.makedirs(data_dir)150 os.makedirs(data_dir)
157 os.chown(data_dir,151 os.chown(data_dir, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid)
158 pwd.getpwnam('ubuntu').pw_uid,
159 grp.getgrnam('ubuntu').gr_gid)
160152
161 configure_cron()153 configure_cron()
162 hookenv.status_set('active', 'Unit is ready')154 hookenv.status_set("active", "Unit is ready")
163155
164156
165@when('config.changed.log_dir')157@when("config.changed.log_dir")
166def configure_log_dir():158def configure_log_dir():
167 """159 """
168 Configure 'log_dir' directory, setup lograte for glance-sync160 Configure 'log_dir' directory, setup lograte for glance-sync
169 log files, and configure cron.161 log files, and configure cron.
170 """162 """
171 hookenv.status_set('maintenance', 'Configuring')163 hookenv.status_set("maintenance", "Configuring")
172 log_dir = hookenv.config('log_dir')164 log_dir = hookenv.config("log_dir")
173 if not os.path.exists(log_dir):165 if not os.path.exists(log_dir):
174 os.makedirs(log_dir)166 os.makedirs(log_dir)
175 os.chown(log_dir,167 os.chown(log_dir, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid)
176 pwd.getpwnam('ubuntu').pw_uid,
177 grp.getgrnam('ubuntu').gr_gid)
178 templating.render(168 templating.render(
179 source='logrotate.d.j2',169 source="logrotate.d.j2",
180 target='/etc/logrotate.d/glance_sync',170 target="/etc/logrotate.d/glance_sync",
181 owner='root',171 owner="root",
182 group='root',172 group="root",
183 perms=0o644,173 perms=0o644,
184 context=hookenv.config(),174 context=hookenv.config(),
185 )175 )
186176
187 configure_cron()177 configure_cron()
188 hookenv.status_set('active', 'Unit is ready')178 hookenv.status_set("active", "Unit is ready")
189179
190180
191@when('config.changed.script_dir')181@when("config.changed.script_dir")
192def configure_script_dir():182def configure_script_dir():
193 """Configure 'script_dir' directory, and configure cron."""183 """Configure 'script_dir' directory, and configure cron."""
194 hookenv.status_set('maintenance', 'Configuring')184 hookenv.status_set("maintenance", "Configuring")
195 script_dir = hookenv.config('script_dir')185 script_dir = hookenv.config("script_dir")
196 if not os.path.exists(script_dir):186 if not os.path.exists(script_dir):
197 os.makedirs(script_dir)187 os.makedirs(script_dir)
198 os.chown(script_dir,188 os.chown(
199 pwd.getpwnam('ubuntu').pw_uid,189 script_dir, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid
200 grp.getgrnam('ubuntu').gr_gid)190 )
201191
202 configure_cron()192 configure_cron()
203 hookenv.status_set('active', 'Unit is ready')193 hookenv.status_set("active", "Unit is ready")
204194
205195
206@when('config.changed.authorized_keys')196@when("config.changed.authorized_keys")
207def configure_authorized_keys():197def configure_authorized_keys():
208 """198 """
209 Configure authorized_keys file containing command-limited199 Configure authorized_keys file containing command-limited
210 public keys for slave(s).200 public keys for slave(s).
211 """201 """
212 hookenv.status_set('maintenance', 'Configuring')202 hookenv.status_set("maintenance", "Configuring")
213 ssh_conf_dir = '/etc/ssh/authorized-keys'203 ssh_conf_dir = "/etc/ssh/authorized-keys"
214 auth_keys_file = os.path.join(ssh_conf_dir,204 auth_keys_file = os.path.join(ssh_conf_dir, "glance-sync-slaves")
215 'glance-sync-slaves')205 authorized_keys = hookenv.config("authorized_keys")
216 authorized_keys = hookenv.config('authorized_keys')
217 if authorized_keys:206 if authorized_keys:
218 keys_bytestring = base64.b64decode(authorized_keys)207 keys_bytestring = base64.b64decode(authorized_keys)
219 keys = str(keys_bytestring, 'utf-8')208 keys = str(keys_bytestring, "utf-8")
220 if not os.path.exists(ssh_conf_dir):209 if not os.path.exists(ssh_conf_dir):
221 os.makedirs(ssh_conf_dir)210 os.makedirs(ssh_conf_dir)
222 os.chown(ssh_conf_dir,211 os.chown(
223 pwd.getpwnam('root').pw_uid,212 ssh_conf_dir, pwd.getpwnam("root").pw_uid, grp.getgrnam("root").gr_gid
224 grp.getgrnam('root').gr_gid)213 )
225 os.chmod(ssh_conf_dir, 0o755)214 os.chmod(ssh_conf_dir, 0o755)
226 with open(auth_keys_file, 'w') as f:215 with open(auth_keys_file, "w") as f:
227 f.write(keys)216 f.write(keys)
228 os.chmod(auth_keys_file, 0o644)217 os.chmod(auth_keys_file, 0o644)
229 configure_sshd(auth_keys_file)218 configure_sshd(auth_keys_file)
230 hookenv.status_set('active', 'Unit is ready')219 hookenv.status_set("active", "Unit is ready")
231 else:220 else:
232 if os.path.isfile(auth_keys_file):221 if os.path.isfile(auth_keys_file):
233 os.remove(auth_keys_file)222 os.remove(auth_keys_file)
234 hookenv.status_set('active', 'Unit is ready')223 hookenv.status_set("active", "Unit is ready")
235224
236225
237def configure_sshd(keys_file):226def configure_sshd(keys_file):
238 """Add 'AuthorizedKeysFile' line to '/etc/ssh/sshd_config'."""227 """Add 'AuthorizedKeysFile' line to '/etc/ssh/sshd_config'."""
239 original = '/etc/ssh/sshd_config'228 original = "/etc/ssh/sshd_config"
240 temp_config = '/tmp/sshd_config'229 temp_config = "/tmp/sshd_config"
241 old_lines = []230 old_lines = []
242 auth_keys_file_added = False231 auth_keys_file_added = False
243 auth_keys_file_existed = False232 auth_keys_file_existed = False
244 homedirs = '%h/.ssh/authorized_keys'233 homedirs = "%h/.ssh/authorized_keys"
245 with open(original, 'r') as old_file:234 with open(original, "r") as old_file:
246 for line in old_file:235 for line in old_file:
247 old_lines.append(line)236 old_lines.append(line)
248 with open(temp_config, 'w') as new_file:237 with open(temp_config, "w") as new_file:
249 for line in old_lines:238 for line in old_lines:
250 if re.search(r'^AuthorizedKeysFile.*{}.*'239 if re.search(r"^AuthorizedKeysFile.*{}.*".format(keys_file), line):
251 .format(keys_file), line):
252 auth_keys_file_existed = True240 auth_keys_file_existed = True
253 new_file.write(line)241 new_file.write(line)
254 continue242 continue
255 else:243 else:
256 replaced = re.sub(r'^AuthorizedKeysFile\s(.*)$',244 replaced = re.sub(
257 r'AuthorizedKeysFile \1 {} {}'245 r"^AuthorizedKeysFile\s(.*)$",
258 .format(homedirs, keys_file),246 r"AuthorizedKeysFile \1 {} {}".format(homedirs, keys_file),
259 line)247 line,
248 )
260 if replaced is not line:249 if replaced is not line:
261 auth_keys_file_added = True250 auth_keys_file_added = True
262 new_file.write(replaced)251 new_file.write(replaced)
263252
264 if not auth_keys_file_existed and not auth_keys_file_added:253 if not auth_keys_file_existed and not auth_keys_file_added:
265 new_file.write('AuthorizedKeysFile {} {}\n'254 new_file.write("AuthorizedKeysFile {} {}\n".format(homedirs, keys_file))
266 .format(homedirs, keys_file))
267255
268 shutil.copy(temp_config, original)256 shutil.copy(temp_config, original)
269 os.system('sudo sshd -t && sudo service ssh reload')257 os.system("sudo sshd -t && sudo service ssh reload")
270258
271259
272def install_slave_sync_script():260def install_slave_sync_script():
@@ -274,22 +262,19 @@ def install_slave_sync_script():
274 Install slave files, and corresponding directory262 Install slave files, and corresponding directory
275 structure.263 structure.
276 """264 """
277 hookenv.status_set('maintenance', 'Installing')265 hookenv.status_set("maintenance", "Installing")
278 hookenv.log('installing slave sync script')266 hookenv.log("installing slave sync script")
279 script_dir = hookenv.config('script_dir')267 script_dir = hookenv.config("script_dir")
280 files = ['glance_sync_slave.py']268 files = ["glance_sync_slave.py"]
281 for file in files:269 for file in files:
282 source = os.path.join(hookenv.charm_dir(),270 source = os.path.join(hookenv.charm_dir(), "files", file)
283 'files', file)
284 dest = os.path.join(script_dir, file)271 dest = os.path.join(script_dir, file)
285 if not os.path.exists(os.path.dirname(dest)):272 if not os.path.exists(os.path.dirname(dest)):
286 os.makedirs(os.path.dirname(dest))273 os.makedirs(os.path.dirname(dest))
287 shutil.copy(source, dest)274 shutil.copy(source, dest)
288 os.chown(dest,275 os.chown(dest, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid)
289 pwd.getpwnam('ubuntu').pw_uid,
290 grp.getgrnam('ubuntu').gr_gid)
291 os.chmod(dest, 0o750)276 os.chmod(dest, 0o750)
292 hookenv.status_set('active', 'Unit is ready')277 hookenv.status_set("active", "Unit is ready")
293278
294279
295def install_master_sync_script():280def install_master_sync_script():
@@ -297,22 +282,19 @@ def install_master_sync_script():
297 Install master files, and corresponding directory282 Install master files, and corresponding directory
298 structure.283 structure.
299 """284 """
300 hookenv.status_set('maintenance', 'Installing')285 hookenv.status_set("maintenance", "Installing")
301 hookenv.log('installing master sync script')286 hookenv.log("installing master sync script")
302 script_dir = hookenv.config('script_dir')287 script_dir = hookenv.config("script_dir")
303 files = ['glance_sync_master.py']288 files = ["glance_sync_master.py"]
304 for file in files:289 for file in files:
305 source = os.path.join(hookenv.charm_dir(),290 source = os.path.join(hookenv.charm_dir(), "files", file)
306 'files', file)
307 dest = os.path.join(script_dir, file)291 dest = os.path.join(script_dir, file)
308 if not os.path.exists(os.path.dirname(dest)):292 if not os.path.exists(os.path.dirname(dest)):
309 os.makedirs(os.path.dirname(dest))293 os.makedirs(os.path.dirname(dest))
310 shutil.copy(source, dest)294 shutil.copy(source, dest)
311 os.chown(dest,295 os.chown(dest, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid)
312 pwd.getpwnam('ubuntu').pw_uid,
313 grp.getgrnam('ubuntu').gr_gid)
314 os.chmod(dest, 0o750)296 os.chmod(dest, 0o750)
315 hookenv.status_set('active', 'Unit is ready')297 hookenv.status_set("active", "Unit is ready")
316298
317299
318def install_db_cleanup_script():300def install_db_cleanup_script():
@@ -320,102 +302,102 @@ def install_db_cleanup_script():
320 Install 'db_purge_deleted_glance_images.py' for302 Install 'db_purge_deleted_glance_images.py' for
321 master or slave.303 master or slave.
322 """304 """
323 hookenv.status_set('maintenance', 'Installing')305 hookenv.status_set("maintenance", "Installing")
324 script_dir = hookenv.config('script_dir')306 script_dir = hookenv.config("script_dir")
325 master_enabled = hookenv.config('master_mode')307 master_enabled = hookenv.config("master_mode")
326 if master_enabled:308 if master_enabled:
327 mode = 'master'309 mode = "master"
328 else:310 else:
329 mode = 'slave'311 mode = "slave"
330 source = os.path.join(hookenv.charm_dir(), 'files',312 source = os.path.join(
331 'db_purge_deleted_' + mode,313 hookenv.charm_dir(),
332 'db_purge_deleted_glance_images.py')314 "files",
333 dest = os.path.join(script_dir,315 "db_purge_deleted_" + mode,
334 'db_purge_deleted_glance_images.py')316 "db_purge_deleted_glance_images.py",
317 )
318 dest = os.path.join(script_dir, "db_purge_deleted_glance_images.py")
335 shutil.copy(source, dest)319 shutil.copy(source, dest)
336 os.chown(dest,320 os.chown(dest, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid)
337 pwd.getpwnam('ubuntu').pw_uid,
338 grp.getgrnam('ubuntu').gr_gid)
339 os.chmod(dest, 0o750)321 os.chmod(dest, 0o750)
340 hookenv.status_set('active', 'Unit is ready')322 hookenv.status_set("active", "Unit is ready")
341323
342324
343@when('config.changed.master_creds')325@when("config.changed.master_creds")
344def configure_master_novarc():326def configure_master_novarc():
345 """327 """
346 Get context from 'master_creds'. Put the info into 'clouds.yaml'328 Get context from 'master_creds'. Put the info into 'clouds.yaml'
347 for the openstacksdk.329 for the openstacksdk.
348 """330 """
349 keystone_creds = config_flags_parser(hookenv.config('master_creds'))331 keystone_creds = config_flags_parser(hookenv.config("master_creds"))
350 clouds_yaml_dir = '/etc/openstack'332 clouds_yaml_dir = "/etc/openstack"
351 if not keystone_creds:333 if not keystone_creds:
352 hookenv.status_set('blocked', 'Please add master_creds')334 hookenv.status_set("blocked", "Please add master_creds")
353 return335 return
354 elif not os.path.exists(clouds_yaml_dir):336 elif not os.path.exists(clouds_yaml_dir):
355 creds = {}337 creds = {}
356 keystone_creds = config_flags_parser(hookenv.config('master_creds'))338 keystone_creds = config_flags_parser(hookenv.config("master_creds"))
357 if '/v3' in keystone_creds['auth_url']:339 if "/v3" in keystone_creds["auth_url"]:
358 creds = {340 creds = {
359 'master_username': keystone_creds['username'],341 "master_username": keystone_creds["username"],
360 'master_password': keystone_creds['password'],342 "master_password": keystone_creds["password"],
361 'master_project': keystone_creds['project'],343 "master_project": keystone_creds["project"],
362 'master_region': keystone_creds['region'],344 "master_region": keystone_creds["region"],
363 'master_auth_url': keystone_creds['auth_url'],345 "master_auth_url": keystone_creds["auth_url"],
364 'master_auth_version': '3',346 "master_auth_version": "3",
365 'master_user_domain': keystone_creds['domain'],347 "master_user_domain": keystone_creds["domain"],
366 'master_project_domain': keystone_creds['domain'],348 "master_project_domain": keystone_creds["domain"],
367 }349 }
368 else:350 else:
369 creds = {351 creds = {
370 'master_username': keystone_creds['username'],352 "master_username": keystone_creds["username"],
371 'master_password': keystone_creds['password'],353 "master_password": keystone_creds["password"],
372 'master_project': keystone_creds['project'],354 "master_project": keystone_creds["project"],
373 'master_region': keystone_creds['region'],355 "master_region": keystone_creds["region"],
374 'master_auth_url': keystone_creds['auth_url'],356 "master_auth_url": keystone_creds["auth_url"],
375 }357 }
376 elif os.path.exists(clouds_yaml_dir):358 elif os.path.exists(clouds_yaml_dir):
377 shutil.rmtree(clouds_yaml_dir, ignore_errors=True)359 shutil.rmtree(clouds_yaml_dir, ignore_errors=True)
378 creds = {}360 creds = {}
379 keystone_creds = config_flags_parser(hookenv.config('master_creds'))361 keystone_creds = config_flags_parser(hookenv.config("master_creds"))
380 if '/v3' in keystone_creds['auth_url']:362 if "/v3" in keystone_creds["auth_url"]:
381 creds = {363 creds = {
382 'master_username': keystone_creds['username'],364 "master_username": keystone_creds["username"],
383 'master_password': keystone_creds['password'],365 "master_password": keystone_creds["password"],
384 'master_project': keystone_creds['project'],366 "master_project": keystone_creds["project"],
385 'master_region': keystone_creds['region'],367 "master_region": keystone_creds["region"],
386 'master_auth_url': keystone_creds['auth_url'],368 "master_auth_url": keystone_creds["auth_url"],
387 'master_auth_version': '3',369 "master_auth_version": "3",
388 'master_user_domain': keystone_creds['domain'],370 "master_user_domain": keystone_creds["domain"],
389 'master_project_domain': keystone_creds['domain'],371 "master_project_domain": keystone_creds["domain"],
390 }372 }
391 else:373 else:
392 creds = {374 creds = {
393 'master_username': keystone_creds['username'],375 "master_username": keystone_creds["username"],
394 'master_password': keystone_creds['password'],376 "master_password": keystone_creds["password"],
395 'master_project': keystone_creds['project'],377 "master_project": keystone_creds["project"],
396 'master_region': keystone_creds['region'],378 "master_region": keystone_creds["region"],
397 'master_auth_url': keystone_creds['auth_url'],379 "master_auth_url": keystone_creds["auth_url"],
398 }380 }
399 clouds_yaml = os.path.join(clouds_yaml_dir, 'clouds.yaml')381 clouds_yaml = os.path.join(clouds_yaml_dir, "clouds.yaml")
400 templating.render(382 templating.render(
401 source='clouds.yaml.j2',383 source="clouds.yaml.j2",
402 target=clouds_yaml,384 target=clouds_yaml,
403 owner='ubuntu',385 owner="ubuntu",
404 group='ubuntu',386 group="ubuntu",
405 perms=0o600,387 perms=0o600,
406 context=creds,388 context=creds,
407 )389 )
408 configure_novarc()390 configure_novarc()
409 hookenv.status_set('active', 'Unit is ready')391 hookenv.status_set("active", "Unit is ready")
410392
411393
412@when('config.changed.novarc')394@when("config.changed.novarc")
413def configure_custom_novarc():395def configure_custom_novarc():
414 """Configure 'novarc' file after config change."""396 """Configure 'novarc' file after config change."""
415 configure_novarc()397 configure_novarc()
416398
417399
418@hook('keystone-admin-relation-{joined,changed}')400@hook("keystone-admin-relation-{joined,changed}")
419def configure_relation_novarc(relation=None):401def configure_relation_novarc(relation=None):
420 """402 """
421 Configure 'novarc' file after adding keystone403 Configure 'novarc' file after adding keystone
@@ -424,7 +406,7 @@ def configure_relation_novarc(relation=None):
424 configure_novarc()406 configure_novarc()
425407
426408
427@hook('database-relation-{joined,changed}')409@hook("database-relation-{joined,changed}")
428def configure_relation_glancedb(relation=None):410def configure_relation_glancedb(relation=None):
429 """411 """
430 Configure 'novarc' file after adding database412 Configure 'novarc' file after adding database
@@ -438,94 +420,88 @@ def configure_novarc():
438 Configure 'novarc' file from user supplied custom novarc420 Configure 'novarc' file from user supplied custom novarc
439 file, or using keystone and mysql relations.421 file, or using keystone and mysql relations.
440 """422 """
441 hookenv.status_set('maintenance', 'Configuring')423 hookenv.status_set("maintenance", "Configuring")
442 keystone_relations = hookenv.relations_of_type('keystone-admin')424 keystone_relations = hookenv.relations_of_type("keystone-admin")
443 db_relations = hookenv.relations_of_type('database')425 db_relations = hookenv.relations_of_type("database")
444 config_dir = hookenv.config('config_dir')426 config_dir = hookenv.config("config_dir")
445 novarc_file = os.path.join(config_dir, 'novarc')427 novarc_file = os.path.join(config_dir, "novarc")
446 custom_novarc = hookenv.config('novarc')428 custom_novarc = hookenv.config("novarc")
447 if len(keystone_relations) > 0 and len(db_relations) > 0:429 if len(keystone_relations) > 0 and len(db_relations) > 0:
448 write_relation_novarc(novarc_file)430 write_relation_novarc(novarc_file)
449 elif not custom_novarc == '':431 elif not custom_novarc == "":
450 write_custom_novarc(novarc_file)432 write_custom_novarc(novarc_file)
451 else:433 else:
452 hookenv.log('ERROR: set novarc config or add keystone and '434 hookenv.log("ERROR: set novarc config or add keystone and database relations")
453 'database relations')
454 if os.path.isfile(novarc_file):435 if os.path.isfile(novarc_file):
455 os.remove(novarc_file)436 os.remove(novarc_file)
456 clear_flag('novarc.configured')437 clear_flag("novarc.configured")
457 hookenv.status_set('blocked', 'Set novarc config or add keystone '438 hookenv.status_set(
458 'and database relations')439 "blocked", "Set novarc config or add keystone and database relations"
440 )
459441
460442
461def write_relation_novarc(path): # noqa: C901 is too complex (14)443def write_relation_novarc(path): # noqa: C901 is too complex (14)
462 """Write 'novarc' file."""444 """Write 'novarc' file."""
463 hookenv.status_set('maintenance', 'Configuring novarc')445 hookenv.status_set("maintenance", "Configuring novarc")
464 hookenv.log('configuring novarc based on keystone relation')446 hookenv.log("configuring novarc based on keystone relation")
465 # TODO: replace this with some better way to get the master region447 # TODO: replace this with some better way to get the master region
466 # name available. Query from the client doesn't have permissions,448 # name available. Query from the client doesn't have permissions,
467 # it is in 'clouds.yaml'. Possible alternative is to use a cross model449 # it is in 'clouds.yaml'. Possible alternative is to use a cross model
468 # relation.450 # relation.
469 context = {}451 context = {}
470 clouds_yaml_dir = '/etc/openstack'452 clouds_yaml_dir = "/etc/openstack"
471 master_creds_set = hookenv.config('master_creds')453 master_creds_set = hookenv.config("master_creds")
472 master_enabled = hookenv.config('master_mode')454 master_enabled = hookenv.config("master_mode")
473 if master_enabled:455 if master_enabled:
474 keystone = hookenv.relations_of_type('keystone-admin')456 keystone = hookenv.relations_of_type("keystone-admin")
475 if len(keystone) > 0:457 if len(keystone) > 0:
476 relation = keystone[0]458 relation = keystone[0]
477 if 'service_password' in relation and relation['service_password']:459 if "service_password" in relation and relation["service_password"]:
478 context['keystone'] = copy(relation)460 context["keystone"] = copy(relation)
479 elif not master_enabled and master_creds_set != '':461 elif not master_enabled and master_creds_set != "":
480 if not os.path.exists(clouds_yaml_dir):462 if not os.path.exists(clouds_yaml_dir):
481 configure_master_novarc()463 configure_master_novarc()
482 keystone_creds = config_flags_parser(hookenv464 keystone_creds = config_flags_parser(hookenv.config("master_creds"))
483 .config('master_creds'))465 context["keystone_master_region"] = keystone_creds["region"]
484 context['keystone_master_region'] = keystone_creds['region']466 keystone = hookenv.relations_of_type("keystone-admin")
485 keystone = hookenv.relations_of_type('keystone-admin')
486 if len(keystone) > 0:467 if len(keystone) > 0:
487 filtered_keystone = ([x for x in keystone468 filtered_keystone = [x for x in keystone if "service_password" in x]
488 if 'service_password' in x])
489 if len(filtered_keystone) > 0:469 if len(filtered_keystone) > 0:
490 context['keystone'] = copy(filtered_keystone[0])470 context["keystone"] = copy(filtered_keystone[0])
491 else:471 else:
492 if master_creds_set == '':472 if master_creds_set == "":
493 hookenv.log('master_creds missing')473 hookenv.log("master_creds missing")
494 hookenv.status_set('blocked',474 hookenv.status_set("blocked", "Please add master_creds")
495 'Please add master_creds')475 mysql = hookenv.relations_of_type("database")
496 mysql = hookenv.relations_of_type('database')
497 if len(mysql) > 0:476 if len(mysql) > 0:
498 relation = mysql[0]477 relation = mysql[0]
499 context['db'] = copy(relation)478 context["db"] = copy(relation)
500 if len(context.keys()) == 2:479 if len(context.keys()) == 2:
501 templating.render(480 templating.render(
502 source='novarc_master.j2',481 source="novarc_master.j2",
503 target=path,482 target=path,
504 owner='ubuntu',483 owner="ubuntu",
505 group='ubuntu',484 group="ubuntu",
506 perms=0o600,485 perms=0o600,
507 context=context,486 context=context,
508 )487 )
509 hookenv.status_set('active', 'Unit is ready')488 hookenv.status_set("active", "Unit is ready")
510 elif len(context.keys()) == 3:489 elif len(context.keys()) == 3:
511 templating.render(490 templating.render(
512 source='novarc_slave.j2',491 source="novarc_slave.j2",
513 target=path,492 target=path,
514 owner='ubuntu',493 owner="ubuntu",
515 group='ubuntu',494 group="ubuntu",
516 perms=0o600,495 perms=0o600,
517 context=context,496 context=context,
518 )497 )
519 hookenv.status_set('active', 'Unit is ready')498 hookenv.status_set("active", "Unit is ready")
520 elif 'db' not in context.keys():499 elif "db" not in context.keys():
521 hookenv.status_set('maintenance',500 hookenv.status_set("maintenance", "mysql relation incomplete")
522 'mysql relation incomplete')501 elif "keystone" not in context.keys():
523 elif 'keystone' not in context.keys():502 hookenv.status_set("maintenance", "keystone relation incomplete")
524 hookenv.status_set('maintenance',
525 'keystone relation incomplete')
526 else:503 else:
527 hookenv.status_set('maintenance', 'keystone and '504 hookenv.status_set("maintenance", "keystone and mysql relation incomplete")
528 'mysql relation incomplete')
529505
530506
531def write_custom_novarc(path):507def write_custom_novarc(path):
@@ -536,172 +512,164 @@ def write_custom_novarc(path):
536 # To add a custom novarc file, run:512 # To add a custom novarc file, run:
537 # `juju config <glance-sync-application>513 # `juju config <glance-sync-application>
538 # novarc=$(base64 -w 0 /path/to/novarc)`514 # novarc=$(base64 -w 0 /path/to/novarc)`
539 hookenv.status_set('maintenance',515 hookenv.status_set("maintenance", "Configuring custom novarc")
540 'Configuring custom novarc')516 hookenv.log("configuring custom novarc")
541 hookenv.log('configuring custom novarc')517 novarc = hookenv.config("novarc")
542 novarc = hookenv.config('novarc')
543 novarc_bytestring = base64.b64decode(novarc)518 novarc_bytestring = base64.b64decode(novarc)
544 with open(path, 'wb') as f:519 with open(path, "wb") as f:
545 f.write(novarc_bytestring)520 f.write(novarc_bytestring)
546 os.chown(path,521 os.chown(path, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid)
547 pwd.getpwnam('ubuntu').pw_uid,
548 grp.getgrnam('ubuntu').gr_gid)
549 os.chmod(path, 0o600)522 os.chmod(path, 0o600)
550 clouds_yaml_dir = '/etc/openstack'523 clouds_yaml_dir = "/etc/openstack"
551 master_creds_set = hookenv.config('master_creds')524 master_creds_set = hookenv.config("master_creds")
552 master_enabled = hookenv.config('master_mode')525 master_enabled = hookenv.config("master_mode")
553 if master_enabled:526 if master_enabled:
554 hookenv.status_set('active', 'Unit is ready')527 hookenv.status_set("active", "Unit is ready")
555 return528 return
556 elif not master_enabled and master_creds_set != '':529 elif not master_enabled and master_creds_set != "":
557 if not os.path.exists(clouds_yaml_dir):530 if not os.path.exists(clouds_yaml_dir):
558 configure_master_novarc()531 configure_master_novarc()
559 hookenv.status_set('active', 'Unit is ready')532 hookenv.status_set("active", "Unit is ready")
560 else:533 else:
561 if master_creds_set == '':534 if master_creds_set == "":
562 hookenv.log('ERROR: master_creds missing')535 hookenv.log("ERROR: master_creds missing")
563 hookenv.status_set('blocked',536 hookenv.status_set("blocked", "Please add master_creds")
564 'Please add master_creds')
565537
566538
567@when('config.changed.sync_source')539@when("config.changed.sync_source")
568def configure_sync_source():540def configure_sync_source():
569 """Configure cron after 'sync_source' config change."""541 """Configure cron after 'sync_source' config change."""
570 configure_cron()542 configure_cron()
571543
572544
573@when_any('config.changed.sync_enabled',545@when_any("config.changed.sync_enabled", "config.changed.cron_frequency")
574 'config.changed.cron_frequency')
575def configure_cron():546def configure_cron():
576 """547 """
577 Configure cron after 'sync_enabled' or548 Configure cron after 'sync_enabled' or
578 'cron_frequency' config change.549 'cron_frequency' config change.
579 """550 """
580 hookenv.status_set('maintenance', 'Configuring')551 hookenv.status_set("maintenance", "Configuring")
581 sync_enabled = hookenv.config('sync_enabled')552 sync_enabled = hookenv.config("sync_enabled")
582 hookenv.log('configuring sync cronjob')553 hookenv.log("configuring sync cronjob")
583 master_enabled = hookenv.config('master_mode')554 master_enabled = hookenv.config("master_mode")
584 if master_enabled:555 if master_enabled:
585 cron_file = '/etc/cron.d/glance_sync_master'556 cron_file = "/etc/cron.d/glance_sync_master"
586 else:557 else:
587 cron_file = '/etc/cron.d/glance_sync_slave'558 cron_file = "/etc/cron.d/glance_sync_slave"
588 if not hookenv.config('sync_source'):559 if not hookenv.config("sync_source"):
589 hookenv.log('ERROR: sync_source not set')560 hookenv.log("ERROR: sync_source not set")
590 hookenv.status_set('blocked', 'Please set a '561 hookenv.status_set(
591 'sync_source to configure crontab')562 "blocked", "Please set a sync_source to configure crontab"
563 )
592 return564 return
593565
594 if sync_enabled and master_enabled:566 if sync_enabled and master_enabled:
595 hookenv.log('adding cronjob')567 hookenv.log("adding cronjob")
596 templating.render(568 templating.render(
597 source='glance_sync_master_cron.j2',569 source="glance_sync_master_cron.j2",
598 target=cron_file,570 target=cron_file,
599 owner='root',571 owner="root",
600 group='root',572 group="root",
601 perms=0o640,573 perms=0o640,
602 context=hookenv.config(),574 context=hookenv.config(),
603 )575 )
604 elif sync_enabled and not master_enabled:576 elif sync_enabled and not master_enabled:
605 # Just an alias to make sure that the paths are as expected.577 # Just an alias to make sure that the paths are as expected.
606 context = hookenv.config()578 context = hookenv.config()
607 if context['sync_source'][-1] != '/':579 if context["sync_source"][-1] != "/":
608 context['sync_source'] += '/'580 context["sync_source"] += "/"
609 if context['data_dir'][-1] != '/':581 if context["data_dir"][-1] != "/":
610 context['data_dir'] += '/'582 context["data_dir"] += "/"
611 hookenv.log('adding cronjob')583 hookenv.log("adding cronjob")
612 templating.render(584 templating.render(
613 source='glance_sync_slave_cron.j2',585 source="glance_sync_slave_cron.j2",
614 target=cron_file,586 target=cron_file,
615 owner='root',587 owner="root",
616 group='root',588 group="root",
617 perms=0o640,589 perms=0o640,
618 context=hookenv.config(),590 context=hookenv.config(),
619 )591 )
620 else:592 else:
621 hookenv.log('removing cronjob')593 hookenv.log("removing cronjob")
622 if os.path.isfile(cron_file):594 if os.path.isfile(cron_file):
623 os.remove(cron_file)595 os.remove(cron_file)
624 clear_flag('cron.configured')596 clear_flag("cron.configured")
625 configure_novarc()597 configure_novarc()
626 hookenv.status_set('active', 'Unit is ready')598 hookenv.status_set("active", "Unit is ready")
627599
628600
629@hook('nrpe-external-master-relation-changed')601@hook("nrpe-external-master-relation-changed")
630def setup_nrpe_checks(nagios):602def setup_nrpe_checks(nagios):
631 """Configure NRPE checks."""603 """Configure NRPE checks."""
632 hookenv.status_set('maintenance', 'Configuring nrpe checks')604 hookenv.status_set("maintenance", "Configuring nrpe checks")
633 config = hookenv.config()605 config = hookenv.config()
634 modes = ['slave', 'master']606 modes = ["slave", "master"]
635607
636 for mode in modes:608 for mode in modes:
637 nagios.add_check(609 nagios.add_check(
638 [610 [
639 '/usr/lib/nagios/plugins/check_file_age',611 "/usr/lib/nagios/plugins/check_file_age",
640 '-w 14400', '-c 25200', '-i', '-f',612 "-w 14400",
641 os.path.join(613 "-c 25200",
642 config['log_dir'],614 "-i",
643 'glance_sync_' + mode + '.log'615 "-f",
644 ),616 os.path.join(config["log_dir"], "glance_sync_" + mode + ".log"),
645 ],617 ],
646 name='glance_sync_' + mode + '_log',618 name="glance_sync_" + mode + "_log",
647 description=(619 description=("Verify age of last image sync from glance to disk"),
648 'Verify age of last image sync '620 context=config["nagios_context"],
649 'from glance to disk'
650 ),
651 context=config['nagios_context'],
652 unit=hookenv.local_unit(),621 unit=hookenv.local_unit(),
653 )622 )
654623
655 # Copy nrpe check plugin for stale lockfiles in place.624 # Copy nrpe check plugin for stale lockfiles in place.
656 script_dir = hookenv.config('script_dir')625 script_dir = hookenv.config("script_dir")
657 files = ['check_stale_lockfile_slave.py',626 files = ["check_stale_lockfile_slave.py", "check_stale_lockfile_master.py"]
658 'check_stale_lockfile_master.py']627 hookenv.log("installing stale lockfile nrpe plugin")
659 hookenv.log('installing stale lockfile nrpe plugin')
660 for file in files:628 for file in files:
661 source = os.path.join(hookenv.charm_dir(), 'files', file)629 source = os.path.join(hookenv.charm_dir(), "files", file)
662 dest = os.path.join(script_dir, file)630 dest = os.path.join(script_dir, file)
663 if not os.path.exists(os.path.dirname(dest)):631 if not os.path.exists(os.path.dirname(dest)):
664 os.makedirs(os.path.dirname(dest))632 os.makedirs(os.path.dirname(dest))
665 shutil.copy(source, dest)633 shutil.copy(source, dest)
666 os.chown(dest,634 os.chown(dest, pwd.getpwnam("ubuntu").pw_uid, grp.getgrnam("ubuntu").gr_gid)
667 pwd.getpwnam('ubuntu').pw_uid,
668 grp.getgrnam('ubuntu').gr_gid)
669 os.chmod(dest, 0o755)635 os.chmod(dest, 0o755)
670636
671 for mode in modes:637 for mode in modes:
672 nrpe_plugin = os.path.join(script_dir,638 nrpe_plugin = os.path.join(
673 'check_stale_lockfile_' + # noqa:W504639 script_dir, "check_stale_lockfile_" + mode + ".py" # noqa:W504
674 mode + '.py')640 )
675 nagios.add_check(641 nagios.add_check(
676 [642 [
677 nrpe_plugin,643 nrpe_plugin,
678 '-f', '/tmp/glance_sync_' + mode + '.lock',644 "-f",
679 '-w 72000', '-c 14400',645 "/tmp/glance_sync_" + mode + ".lock",
646 "-w 72000",
647 "-c 14400",
680 ],648 ],
681 name='glance_sync_' + mode + '_lockfile',649 name="glance_sync_" + mode + "_lockfile",
682 description='Verify age of image sync lockfile',650 description="Verify age of image sync lockfile",
683 context=config['nagios_context'],651 context=config["nagios_context"],
684 unit=hookenv.local_unit(),652 unit=hookenv.local_unit(),
685 )653 )
686654
687 hookenv.status_set('active', 'Unit is ready')655 hookenv.status_set("active", "Unit is ready")
688656
689657
690@when('config.changed.trusted_ssl_ca')658@when("config.changed.trusted_ssl_ca")
691def fix_ssl():659def fix_ssl():
692 """660 """
693 Write user supplied SSL CA from 'trusted_ssl_ca' to661 Write user supplied SSL CA from 'trusted_ssl_ca' to
694 'cert_file', and run `update-ca-certificates`.662 'cert_file', and run `update-ca-certificates`.
695 """663 """
696 cert_file = '/usr/local/share/ca-certificates/openstack-service-checks.crt'664 cert_file = "/usr/local/share/ca-certificates/openstack-service-checks.crt"
697 config = hookenv.config()665 config = hookenv.config()
698 trusted_ssl_ca = config.get('trusted_ssl_ca').strip()666 trusted_ssl_ca = config.get("trusted_ssl_ca").strip()
699 hookenv.log('writing ssl ca cert:{}'.format(trusted_ssl_ca))667 hookenv.log("writing ssl ca cert:{}".format(trusted_ssl_ca))
700 cert_content = base64.b64decode(trusted_ssl_ca).decode()668 cert_content = base64.b64decode(trusted_ssl_ca).decode()
701 with open(cert_file, 'w') as f:669 with open(cert_file, "w") as f:
702 print(cert_content, file=f)670 print(cert_content, file=f)
703 try:671 try:
704 subprocess.call(['/usr/sbin/update-ca-certificates'])672 subprocess.call(["/usr/sbin/update-ca-certificates"])
705 except subprocess.CalledProcessError as e:673 except subprocess.CalledProcessError as e:
706 hookenv.log('ERROR: fix_ssl() failed with {}'.format(e))674 hookenv.log("ERROR: fix_ssl() failed with {}".format(e))
707 hookenv.status_set('error', 'CA cert update failed')675 hookenv.status_set("error", "CA cert update failed")
diff --git a/src/tests/functional/tests/test_glance_sync.py b/src/tests/functional/tests/test_glance_sync.py
index e03511b..0d6e981 100644
--- a/src/tests/functional/tests/test_glance_sync.py
+++ b/src/tests/functional/tests/test_glance_sync.py
@@ -48,9 +48,10 @@ class BaseGlanceSyncTest(unittest.TestCase):
48 )48 )
49 b64_ssh_key = base64.b64encode(cls.slave_ssh_key.encode())49 b64_ssh_key = base64.b64encode(cls.slave_ssh_key.encode())
50 master_config = {"authorized_keys": b64_ssh_key.decode()}50 master_config = {"authorized_keys": b64_ssh_key.decode()}
51 slave_config = {"master_creds": master_creds,51 slave_config = {
52 "sync_source": "ubuntu@{}:/srv/glance_sync/data".format(cls.master_ip),52 "master_creds": master_creds,
53 }53 "sync_source": "ubuntu@{}:/srv/glance_sync/data".format(cls.master_ip),
54 }
54 model.set_application_config(cls.master_app, master_config)55 model.set_application_config(cls.master_app, master_config)
55 model.set_application_config(cls.slave_app, slave_config)56 model.set_application_config(cls.slave_app, slave_config)
5657
@@ -72,23 +73,24 @@ class CharmOperationTest(BaseGlanceSyncTest):
72 # Upload a Glance image to use for test, doesn't need to be big or even real73 # Upload a Glance image to use for test, doesn't need to be big or even real
73 openstack.enable_logging(debug=True)74 openstack.enable_logging(debug=True)
74 conn_master = openstack.connection.Connection(75 conn_master = openstack.connection.Connection(
75 region_name='RegionOne',76 region_name="RegionOne",
76 auth=dict(77 auth=dict(
77 auth_url='http://{}:35357/v3'.format(self.keystone_master_ip),78 auth_url="http://{}:35357/v3".format(self.keystone_master_ip),
78 username='admin',79 username="admin",
79 password=self.master_password,80 password=self.master_password,
80 project_name='admin',81 project_name="admin",
81 user_domain_name='admin_domain',82 user_domain_name="admin_domain",
82 project_domain_name='admin_domain',83 project_domain_name="admin_domain",
83 ),84 ),
84 compute_api_version='2',85 compute_api_version="2",
85 identity_interface='public')86 identity_interface="public",
87 )
86 image_attrs = {88 image_attrs = {
87 'name': 'zaza_test_image',89 "name": "zaza_test_image",
88 'data': 'some_test_data',90 "data": "some_test_data",
89 'disk_format': 'raw',91 "disk_format": "raw",
90 'container_format': 'bare',92 "container_format": "bare",
91 'visibility': 'public',93 "visibility": "public",
92 }94 }
93 self.image = conn_master.image.upload_image(**image_attrs)95 self.image = conn_master.image.upload_image(**image_attrs)
94 # Has image.id, image.created, image.name for future use96 # Has image.id, image.created, image.name for future use
@@ -97,21 +99,22 @@ class CharmOperationTest(BaseGlanceSyncTest):
97 def test_02_check_slave(self):99 def test_02_check_slave(self):
98 """Check that the previously uploaded image lands on the slave.100 """Check that the previously uploaded image lands on the slave.
99101
100 Run a loop till timeout, that checks Glance in the slave region for the presence of the102 Run a loop until timeout, that checks glance in the slave region
101 image uploaded to the master.103 for the presence of the image uploaded to the master.
102 """104 """
103 conn_slave = openstack.connection.Connection(105 conn_slave = openstack.connection.Connection(
104 region_name='RegionOne',106 region_name="RegionOne",
105 auth=dict(107 auth=dict(
106 auth_url='http://{}:35357/v3'.format(self.keystone_slave_ip),108 auth_url="http://{}:35357/v3".format(self.keystone_slave_ip),
107 username='admin',109 username="admin",
108 password=self.slave_password,110 password=self.slave_password,
109 project_name='admin',111 project_name="admin",
110 user_domain_name='admin_domain',112 user_domain_name="admin_domain",
111 project_domain_name='admin_domain',113 project_domain_name="admin_domain",
112 ),114 ),
113 compute_api_version='2',115 compute_api_version="2",
114 identity_interface='public')116 identity_interface="public",
117 )
115 timeout = time.time() + TEST_TIMEOUT118 timeout = time.time() + TEST_TIMEOUT
116 while time.time() < timeout:119 while time.time() < timeout:
117 image_list = conn_slave.get_image("zaza_test_image")120 image_list = conn_slave.get_image("zaza_test_image")
diff --git a/src/tests/unit/__init__.py b/src/tests/unit/__init__.py
index 03acc40..28e9795 100644
--- a/src/tests/unit/__init__.py
+++ b/src/tests/unit/__init__.py
@@ -1,2 +1,3 @@
1import sys1import sys
2sys.path.append('.')2
3sys.path.append(".")
diff --git a/src/tox.ini b/src/tox.ini
index cd1b1f7..45ced91 100644
--- a/src/tox.ini
+++ b/src/tox.ini
@@ -25,7 +25,7 @@ passenv =
25[testenv:lint]25[testenv:lint]
26commands =26commands =
27 flake827 flake8
28#TODO black --check --exclude "/(\.eggs|\.git|\.tox|\.venv|\.build|dist|charmhelpers|mod)/" .28 black --check --exclude "/(\.eggs|\.git|\.tox|\.venv|\.build|dist|charmhelpers|mod)/" .
29deps =29deps =
30 black30 black
31 flake831 flake8
@@ -45,8 +45,7 @@ exclude =
45 mod,45 mod,
46 .build46 .build
4747
48max-line-length = 12048max-line-length = 88
49#TODO max-line-length = 88
50max-complexity = 1049max-complexity = 10
5150
52[testenv:black]51[testenv:black]

Subscribers

People subscribed via source and target branches

to all changes: