blob: ae2ebd3b0d29deb464ccb552d77823b07b1525a0 [file] [log] [blame]
Vasyl Saienko59096f22018-11-26 17:26:42 +02001#!/usr/bin/env python
2#
3# Copyright 2018 Mirantis, Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17
18"""Growpart python module
19
20The module is aimed to extend logical volumes according to sizes provided
21in image layout.
22
23Example:
24 python growlvm --image-layout-file '/root/mylayout.yml'
25
26Attributes:
27 image-layout - json string with image layout. Supported params and
28 description might be found in IMAGE_LAYOUT_SCHEMA
29
30"""
31
32__version__ = '1.0'
33
34import argparse
35import collections
36import yaml
37from jsonschema import validate
38import logging
39import os
40import re
41import subprocess
42import sys
43
44
45LOG = logging.getLogger(__name__)
46
47DECIMAL_REG = re.compile(r"(\d+)")
48
49IMAGE_LAYOUT_SCHEMA = {
50 "$schema": "http://json-schema.org/draft-04/schema#",
51 "title": "Image partition layout",
52 "type": "object",
53 "patternProperties": {
54 ".*": {"$ref": "#/definitions/logical_volume_layout"}
55 },
56 "definitions": {
57 "logical_volume_layout": {
58 "type": "object",
59 "properties": {
60 "name": {
61 "description": "Logical Volume Name",
62 "type": "string"
63 },
64 "size": {
65 "description": (
66 "Size of Logical volume in units of logical extents. "
67 "The number might be volume size in units of "
68 "megabytes. A size suffix of M for megabytes, G for "
69 "gigabytes, T for terabytes, P for petabytes or E for "
70 "exabytes is optional. The number can also be "
71 "expressed as a percentage of the total space in the "
72 "Volume Group with the suffix %VG. Percentage of the "
73 "changeble values like free space is not supported."
74 ),
75 },
76 "resizefs": {
77 "description": (
78 "Resize underlying filesystem together with the "
79 "logical volume using fsadm(8)."
80 ),
81 "type": "boolean"
82 },
83 "vg": {
84 "description": ("Volume group name to resize logical "
85 "volume on."),
86 "type": "string"
87 }
88 },
89 "additionalProperties": False,
90 "required": ["size"]
91 }
92 },
93}
94
95
96def get_volume_groups_info(unit, vg):
97 cmd = ("vgs --noheadings -o vg_name,size,free,vg_extent_size --units %s "
98 "--separator ';' %s") % (unit, vg)
99 try:
100 output = subprocess.check_output(cmd, shell=True,
101 stderr=subprocess.STDOUT)
102 except subprocess.CalledProcessError as exc:
103 raise Exception("Failed to get volume group info", exc.output)
104
105 vgs = []
106 for line in output.splitlines():
107 parts = line.strip().split(';')
108 vgs.append({
109 'name': parts[0],
110 'size': int(DECIMAL_REG.match(parts[1]).group(1)),
111 'free': int(DECIMAL_REG.match(parts[2]).group(1)),
112 'ext_size': int(DECIMAL_REG.match(parts[3]).group(1))
113 })
114 return vgs
115
116
117def get_logical_volume_info(unit, vg):
118 cmd = ("lvs -a --noheadings --nosuffix -o lv_name,size,lv_attr --units %s "
119 "--separator ';' %s") % (unit, vg)
120 try:
121 output = subprocess.check_output(cmd, shell=True,
122 stderr=subprocess.STDOUT)
123 except subprocess.CalledProcessError as exc:
124 raise Exception("Failed to get volume info", exc.output)
125
126 lvs = []
127
128 for line in output.splitlines():
129 parts = line.strip().split(';')
130 lvs.append({
131 'name': parts[0].replace('[', '').replace(']', ''),
132 'size': int(DECIMAL_REG.match(parts[1]).group(1)),
133 })
134 return lvs
135
136
137def normalize_to_pe(size, pe):
138 """ Make sure requested size is multiply of PE
139
140 PE is gathered from system with honor of provided units,
141 when volume size is set in Gigabytes, PE (4mb default) will
142 be shown as 0.
143 """
144
145 if pe > 0:
146 return (size // pe + 1) * pe
147
148 return size
149
150
151def main():
152 logging.basicConfig(
153 format='%(asctime)s - %(levelname)s - %(message)s'
154 )
155 LOG.setLevel(logging.INFO)
156
157 parser = argparse.ArgumentParser(
158 description=('Grow lvm partitions and theirs filesystems to '
159 'specified sizes.')
160 )
161
162 group = parser.add_mutually_exclusive_group(required=True)
163 group.add_argument(
164 '--image-layout',
165 help='json based image layout',
166 )
167 group.add_argument(
168 '--image-layout-file',
169 help='Path to file with image layout',
170 )
171 args = parser.parse_args()
172
173 if args.image_layout_file:
174 with open(args.image_layout_file) as f:
175 layout_info = yaml.load(f)
176 else:
177 layout_info = yaml.load(args.image_layout)
178
179 LOG.info("Validating provided layout.")
180 try:
181 validate(layout_info, IMAGE_LAYOUT_SCHEMA)
182 except Exception as e:
183 LOG.error("Validation of provided layout failed.")
184 raise e
185
186 for part_name, part_layout in layout_info.iteritems():
187
188 size_opt = '--size'
189 size_unit = 'm'
190
191 size = part_layout['size']
192 vg = part_layout.get('vg', 'vg0')
193 resizefs = part_layout.get('resizefs', True)
194
195 if size:
196 if '+' in size:
197 raise Exception("Setting relative size is not supported.")
198 # LVCREATE(8) -l --extents option with percentage
199 elif '%' in size:
200 size_parts = size.split('%', 1)
201 size_percent = int(size_parts[0])
202 if size_percent > 100:
203 raise Exception(
204 "Size percentage cannot be larger than 100%")
205 size_whole = size_parts[1]
206 if size_whole == 'ORIGIN':
207 raise Exception("Snapshot Volumes are not supported")
208 elif size_whole not in ['VG']:
209 raise Exception("Relative sizes are not supported.")
210 size_opt = '--extents'
211 size_unit = ''
212 else:
213 # LVCREATE(8) -L --size option unit
214 if size[-1].lower() in 'bskmgtpe':
215 size_unit = size[-1].lower()
216 size = size[0:-1]
217
218 # when no unit, megabytes by default
219 if size_opt == '--extents':
220 unit = 'm'
221 else:
222 unit = size_unit
223
224 vgs = get_volume_groups_info(unit, vg)
225 this_volume_group = vgs[0]
226 pe = this_volume_group['ext_size']
227
228 lvs = get_logical_volume_info(unit, vg)
229
230 LOG.info("Volume group info: %s", vgs)
231 LOG.info("Logical Volume info: %s", lvs)
232
233 this_volume = [v for v in lvs if v['name'] == part_name][0]
234
235 LOG.info("Using %s for volume: %s", size_opt, this_volume)
236 if size_opt == '--extents':
237 size_free = this_volume_group['free']
238 if size_whole == 'VG':
239 size_requested = normalize_to_pe(
240 size_percent * this_volume_group['size'] / 100, pe)
241
242 LOG.info("Request %s size for volume %s",
243 size_requested, this_volume)
244 if this_volume['size'] > size_requested:
245 raise Exception("Redusing volume size in not supported.")
246 elif this_volume['size'] < size_requested:
247 if (size_free > 0) and (('+' not in size) or
248 (size_free >= (size_requested - this_volume['size']))):
249 cmd = "lvextend "
250 else:
251 raise Exception(
252 ("Logical Volume %s could not be extended. Not "
253 "enough free space left (%s%s required / %s%s "
254 "available)"),
255 this_volume['name'],
256 size_requested - this_volume['size'],
257 unit, size_free, unit
258 )
259 if resizefs:
260 cmd += "--resizefs "
261
262 cmd = "%s -%s %s%s %s/%s" % (
263 cmd, size_opt, size, size_unit, vg, this_volume['name'])
264 try:
265 LOG.debug("Executing command: %s", cmd)
266 output = subprocess.check_output(
267 cmd,
268 shell=True,
269 stderr=subprocess.STDOUT)
270 except subprocess.CalledProcessError as exc:
271 raise Exception(
272 "Failed to resize volume %s. Exception: %s" % (
273 part_name, exc.output))
274 else:
275 LOG.info("No need to resize volume %s.", part_name)
276 else:
277 cmd = "lvresize "
278 if normalize_to_pe(int(size), pe) > this_volume['size']:
279 if resizefs:
280 cmd += "--resizefs "
281 cmd = "%s -%s %s%s %s/%s" % (
282 cmd, size_opt, size, size_unit, vg, this_volume['name'])
283 try:
284 LOG.debug("Executing command: %s", cmd)
285 output = subprocess.check_output(
286 cmd,
287 shell=True,
288 stderr=subprocess.STDOUT)
289 except subprocess.CalledProcessError as exc:
290 raise Exception(
291 "Failed to resize volume %s. Exception: %s" % (
292 part_name, exc.output))
293
294 elif normalize_to_pe(int(size), pe) == this_volume['size']:
295 LOG.info("No need to resize volume %s.", part_name)
296 else:
297 raise Exception(
298 "Redusing size in not supported for provided layout.")
299
300
301if __name__ == '__main__':
302 try:
303 main()
304 except Exception as e:
305 LOG.exception("Failed to apply image layout: %s", e)
306 sys.exit(1)