Revert "Revert "oran-shell-release: release image for F""
[pti/rtp.git] / meta-starlingx / meta-stx-virt / recipes-extended / ceph / files / ceph-manage-journal.py
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2019 Wind River Systems, Inc.
4 #
5 # SPDX-License-Identifier: Apache-2.0
6 #
7
8 import ast
9 import os
10 import os.path
11 import re
12 import subprocess
13 import sys
14
15 DEVICE_NAME_NVME = "nvme"
16
17 #########
18 # Utils #
19 #########
20
21
22 def command(arguments, **kwargs):
23     """Execute e command and capture stdout, stderr & return code"""
24     process = subprocess.Popen(
25         arguments,
26         stdout=subprocess.PIPE,
27         stderr=subprocess.PIPE,
28         **kwargs)
29     out, err = process.communicate()
30     return out, err, process.returncode
31
32
33 def get_input(arg, valid_keys):
34     """Convert the input to a dict and perform basic validation"""
35     json_string = arg.replace("\\n", "\n")
36     try:
37         input_dict = ast.literal_eval(json_string)
38         if not all(k in input_dict for k in valid_keys):
39             return None
40     except Exception:
41         return None
42
43     return input_dict
44
45
46 def get_partition_uuid(dev):
47     output, _, _ = command(['blkid', dev])
48     try:
49         return re.search('PARTUUID=\"(.+?)\"', output).group(1)
50     except AttributeError:
51         return None
52
53
54 def device_path_to_device_node(device_path):
55     try:
56         output, _, _ = command(["udevadm", "settle", "-E", device_path])
57         out, err, retcode = command(["readlink", "-f", device_path])
58         out = out.rstrip()
59     except Exception as e:
60         return None
61
62     return out
63
64
65 ###########################################
66 # Manage Journal Disk Partitioning Scheme #
67 ###########################################
68
69 DISK_BY_PARTUUID = "/dev/disk/by-partuuid/"
70 JOURNAL_UUID = '45b0969e-9b03-4f30-b4c6-b4b80ceff106'  # Type of a journal partition
71
72
73 def is_partitioning_correct(disk_path, partition_sizes):
74     """Validate the existence and size of journal partitions"""
75
76     # Obtain the device node from the device path.
77     disk_node = device_path_to_device_node(disk_path)
78
79     # Check that partition table format is GPT
80     output, _, _ = command(["udevadm", "settle", "-E", disk_node])
81     output, _, _ = command(["parted", "-s", disk_node, "print"])
82     if not re.search('Partition Table: gpt', output):
83         print("Format of disk node %s is not GPT, zapping disk" % disk_node)
84         return False
85
86     # Check each partition size
87     partition_index = 1
88     for size in partition_sizes:
89         # Check that each partition size matches the one in input
90         if DEVICE_NAME_NVME in disk_node:
91             partition_node = '{}p{}'.format(disk_node, str(partition_index))
92         else:
93             partition_node = '{}{}'.format(disk_node, str(partition_index))
94
95         output, _, _ = command(["udevadm", "settle", "-E", partition_node])
96         cmd = ["parted", "-s", partition_node, "unit", "MiB", "print"]
97         output, _, _ = command(cmd)
98
99         regex = ("^Disk " + str(partition_node) + ":\\s*" +
100                  str(size) + "[\\.0]*MiB")
101         if not re.search(regex, output, re.MULTILINE):
102             print("Journal partition %(node)s size is not %(size)s, "
103                   "zapping disk" % {"node": partition_node, "size": size})
104             return False
105
106         partition_index += 1
107
108     output, _, _ = command(["udevadm", "settle", "-t", "10"])
109     return True
110
111
112 def create_partitions(disk_path, partition_sizes):
113     """Recreate partitions"""
114
115     # Obtain the device node from the device path.
116     disk_node = device_path_to_device_node(disk_path)
117
118     # Issue: After creating a new partition table on a device, Udev does not
119     # always remove old symlinks (i.e. to previous partitions on that device).
120     # Also, even if links are erased before zapping the disk, some of them will
121     # be recreated even though there is no partition to back them!
122     # Therefore, we have to remove the links AFTER we erase the partition table
123     # Issue: DISK_BY_PARTUUID directory is not present at all if there are no
124     # GPT partitions on the storage node so nothing to remove in this case
125     links = []
126     if os.path.isdir(DISK_BY_PARTUUID):
127         links = [os.path.join(DISK_BY_PARTUUID, l) for l in os.listdir(DISK_BY_PARTUUID)
128                  if os.path.islink(os.path.join(DISK_BY_PARTUUID, l))]
129
130     # Erase all partitions on current node by creating a new GPT table
131     _, err, ret = command(["parted", "-s", disk_node, "mktable", "gpt"])
132     if ret:
133         print("Error erasing partition table of %(node)s\n"
134               "Return code: %(ret)s reason: %(reason)s" %
135               {"node": disk_node, "ret": ret, "reason": err})
136         exit(1)
137
138     # Erase old symlinks
139     for l in links:
140         if disk_node in os.path.realpath(l):
141             os.remove(l)
142
143     # Create partitions in order
144     used_space_mib = 1  # leave 1 MB at the beginning of the disk
145     num = 1
146     for size in partition_sizes:
147         cmd = ['parted', '-s', disk_node, 'unit', 'mib',
148                'mkpart', 'primary',
149                str(used_space_mib), str(used_space_mib + size)]
150         _, err, ret = command(cmd)
151         parms = {"disk_node": disk_node,
152                  "start": used_space_mib,
153                  "end": used_space_mib + size,
154                  "reason": err}
155         print("Created partition from start=%(start)s MiB to end=%(end)s MiB"
156               " on %(disk_node)s" % parms)
157         if ret:
158             print("Failed to create partition with "
159                   "start=%(start)s, end=%(end)s "
160                   "on %(disk_node)s reason: %(reason)s" % parms)
161             exit(1)
162         # Set partition type to ceph journal
163         # noncritical operation, it makes 'ceph-disk list' output correct info
164         cmd = ['sgdisk',
165                '--change-name={num}:ceph journal'.format(num=num),
166                '--typecode={num}:{uuid}'.format(
167                    num=num,
168                    uuid=JOURNAL_UUID,
169                ),
170                disk_node]
171         _, err, ret = command(cmd)
172         if ret:
173             print("WARNINIG: Failed to set partition name and typecode")
174         used_space_mib += size
175         num += 1
176
177
178 ###########################
179 # Manage Journal Location #
180 ###########################
181
182 OSD_PATH = "/var/lib/ceph/osd/"
183
184
185 def mount_data_partition(data_path, osdid):
186     """Mount an OSD data partition and return the mounted path"""
187
188     # Obtain the device node from the device path.
189     data_node = device_path_to_device_node(data_path)
190
191     mount_path = OSD_PATH + "ceph-" + str(osdid)
192     output, _, _ = command(['mount'])
193     regex = "^" + data_node + ".*" + mount_path
194     if not re.search(regex, output, re.MULTILINE):
195         cmd = ['mount', '-t', 'xfs', data_node, mount_path]
196         _, _, ret = command(cmd)
197         params = {"node": data_node, "path": mount_path}
198         if ret:
199             print("Failed to mount %(node)s to %(path), aborting" % params)
200             exit(1)
201         else:
202             print("Mounted %(node)s to %(path)s" % params)
203     return mount_path
204
205
206 def is_location_correct(path, journal_path, osdid):
207     """Check if location points to the correct device"""
208
209     # Obtain the device node from the device path.
210     journal_node = device_path_to_device_node(journal_path)
211
212     cur_node = os.path.realpath(path + "/journal")
213     if cur_node == journal_node:
214         return True
215     else:
216         return False
217
218
219 def fix_location(mount_point, journal_path, osdid):
220     """Move the journal to the new partition"""
221
222     # Obtain the device node from the device path.
223     journal_node = device_path_to_device_node(journal_path)
224
225     # Fix symlink
226     path = mount_point + "/journal"  # 'journal' symlink path used by ceph-osd
227     journal_uuid = get_partition_uuid(journal_node)
228     new_target = DISK_BY_PARTUUID + journal_uuid
229     params = {"path": path, "target": new_target}
230     try:
231         if os.path.lexists(path):
232             os.unlink(path)  # delete the old symlink
233         os.symlink(new_target, path)
234         print("Symlink created: %(path)s -> %(target)s" % params)
235     except:
236         print("Failed to create symlink: %(path)s -> %(target)s" % params)
237         exit(1)
238     # Fix journal_uuid
239     path = mount_point + "/journal_uuid"
240     try:
241         with open(path, 'w') as f:
242             f.write(journal_uuid)
243     except Exception as ex:
244         # The operation is noncritical, it only makes 'ceph-disk list'
245         # display complete output. We log and continue.
246         params = {"path": path, "uuid": journal_uuid}
247         print("WARNING: Failed to set uuid of %(path)s to %(uuid)s" % params)
248
249     # Clean the journal partition
250     # even if erasing the partition table, if another journal was present here
251     # it's going to be reused. Journals are always bigger than 100MB.
252     command(['dd', 'if=/dev/zero', 'of=%s' % journal_node,
253              'bs=1M', 'count=100'])
254
255     # Format the journal
256     cmd = ['/usr/bin/ceph-osd', '-i', str(osdid),
257            '--pid-file', '/var/run/ceph/osd.%s.pid' % osdid,
258            '-c', '/etc/ceph/ceph.conf',
259            '--cluster', 'ceph',
260            '--mkjournal']
261     out, err, ret = command(cmd)
262     params = {"journal_node": journal_node,
263               "osdid": osdid,
264               "ret": ret,
265               "reason": err}
266     if not ret:
267         print("Prepared new journal partition: %(journal_node)s "
268               "for osd id: %(osdid)s" % params)
269     else:
270         print("Error initializing journal node: "
271               "%(journal_node)s for osd id: %(osdid)s "
272               "ceph-osd return code: %(ret)s reason: %(reason)s" % params)
273
274
275 ########
276 # Main #
277 ########
278
279 def main(argv):
280     # parse and validate arguments
281     err = False
282     partitions = None
283     location = None
284     if len(argv) != 2:
285         err = True
286     elif argv[0] == "partitions":
287         valid_keys = ['disk_path', 'journals']
288         partitions = get_input(argv[1], valid_keys)
289         if not partitions:
290             err = True
291         elif not isinstance(partitions['journals'], list):
292             err = True
293     elif argv[0] == "location":
294         valid_keys = ['data_path', 'journal_path', 'osdid']
295         location = get_input(argv[1], valid_keys)
296         if not location:
297             err = True
298         elif not isinstance(location['osdid'], int):
299             err = True
300     else:
301         err = True
302     if err:
303         print("Command intended for internal use only")
304         exit(-1)
305
306     if partitions:
307         # Recreate partitions only if the existing ones don't match input
308         if not is_partitioning_correct(partitions['disk_path'],
309                                        partitions['journals']):
310             create_partitions(partitions['disk_path'], partitions['journals'])
311         else:
312             print("Partition table for %s is correct, "
313                   "no need to repartition" %
314                   device_path_to_device_node(partitions['disk_path']))
315     elif location:
316         # we need to have the data partition mounted & we can let it mounted
317         mount_point = mount_data_partition(location['data_path'],
318                                            location['osdid'])
319         # Update journal location only if link point to another partition
320         if not is_location_correct(mount_point,
321                                    location['journal_path'],
322                                    location['osdid']):
323             print("Fixing journal location for "
324                   "OSD id: %(id)s" % {"node": location['data_path'],
325                                       "id": location['osdid']})
326             fix_location(mount_point,
327                          location['journal_path'],
328                          location['osdid'])
329         else:
330             print("Journal location for %s is correct,"
331                   "no need to change it" % location['data_path'])
332
333
334 main(sys.argv[1:])