[PATCH 0/2] Using existing VG as storage pool

Team, this work is not completed. I'm sending it to ML anyway because probably someone else will continue on it. What it has to be done: - implement test; - finish the frontend part: - storage pool name textfield cannot be edited if user select a VG (that vgname should populate the storage pool name automatically). - fix any UI bug. - create error messages accordingly (error codes used in frontend are mockups); - Add i18n for tmpl file. The backend looks ok but I made some quick tests during the development. I'll be reading my e-mails during the residency so I'm pretty sure we can work together. Thanks Jose Ricardo Ziviani (2): Implement the backend to use existing VG as storage pool Implement the frontend to use existing VG as storage pool src/wok/plugins/kimchi/API.json | 5 + src/wok/plugins/kimchi/control/host.py | 22 ++++ src/wok/plugins/kimchi/disks.py | 123 +++++++++++++++++++++ src/wok/plugins/kimchi/i18n.py | 3 + src/wok/plugins/kimchi/model/host.py | 30 +++++ src/wok/plugins/kimchi/model/storagepools.py | 7 +- .../kimchi/ui/css/theme-default/storage.css | 22 ++++ src/wok/plugins/kimchi/ui/js/src/kimchi.api.js | 11 ++ .../ui/js/src/kimchi.storagepool_add_main.js | 58 +++++++++- .../kimchi/ui/pages/storagepool-add.html.tmpl | 38 ++++++- 10 files changed, 313 insertions(+), 6 deletions(-) -- 1.9.1

- This commit implements the backend for users to create a storage pool using an existing backend in the system. Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- src/wok/plugins/kimchi/control/host.py | 22 +++++ src/wok/plugins/kimchi/disks.py | 123 +++++++++++++++++++++++++++ src/wok/plugins/kimchi/i18n.py | 3 + src/wok/plugins/kimchi/model/host.py | 30 +++++++ src/wok/plugins/kimchi/model/storagepools.py | 7 +- 5 files changed, 184 insertions(+), 1 deletion(-) diff --git a/src/wok/plugins/kimchi/control/host.py b/src/wok/plugins/kimchi/control/host.py index 9fe4c0a..1fa236e 100644 --- a/src/wok/plugins/kimchi/control/host.py +++ b/src/wok/plugins/kimchi/control/host.py @@ -42,6 +42,28 @@ class Host(Resource): self.swupdate = self.generate_action_handler_task('swupdate') self.swupdateprogress = SoftwareUpdateProgress(self.model) self.cpuinfo = CPUInfo(self.model) + self.vgs = VolumeGroups(self.model) + + @property + def data(self): + return self.info + + +class VolumeGroups(Collection): + def __init__(self, model): + super(VolumeGroups, self).__init__(model) + self.role_key = 'host' + self.uri_fmt = "/host/vgs" + self.admin_methods = ['GET'] + self.resource = VolumeGroup + + +class VolumeGroup(Resource): + def __init__(self, model, id=None): + super(VolumeGroup, self).__init__(model, id) + self.role_key = 'host' + self.uri_fmt = "/host/vgs/%s" + self.admin_methods = ['GET'] @property def data(self): diff --git a/src/wok/plugins/kimchi/disks.py b/src/wok/plugins/kimchi/disks.py index eb40e3a..30a52b9 100644 --- a/src/wok/plugins/kimchi/disks.py +++ b/src/wok/plugins/kimchi/disks.py @@ -194,3 +194,126 @@ def get_partition_details(name): dev['path'] = dev_path dev['name'] = name return dev + + +def vgs(): + """ + lists all volume groups in the system. All size units are in bytes. + + [{'vgname': 'vgtest', 'size': 999653638144L, 'free': 0}] + """ + cmd = ['vgs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'vg_name,vg_size,vg_free'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of VGs + vgs = map(lambda v: v.strip(), out.strip('\n').split('\n')) + + # create a dict based on data retrieved from vgs + return map(lambda l: {'vgname': l[0], + 'size': long(l[1]), + 'free': long(l[2])}, + [fields.split() for fields in vgs]) + + +def lvs(vgname=None): + """ + lists all logical volumes found in the system. It can be filtered by + the volume group. All size units are in bytes. + + [{'lvname': 'lva', 'path': '/dev/vgtest/lva', 'size': 12345L}, + {'lvname': 'lvb', 'path': '/dev/vgtest/lvb', 'size': 12345L}] + """ + cmd = ['lvs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'lv_name,lv_path,lv_size,vg_name'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of LVs filtered by vgname, if + # provided + lvs = filter(lambda f: vgname is None or vgname in f, + map(lambda v: v.strip(), out.strip('\n').split('\n'))) + + # create a dict based on data retrieved from lvs + return map(lambda l: {'lvname': l[0], + 'path': l[1], + 'size': long(l[2])}, + [fields.split() for fields in lvs]) + + +def pvs(vgname=None): + """ + lists all physical volumes in the system. It can be filtered by the + volume group. All size units are in bytes. + + [{'pvname': '/dev/sda3', + 'size': 469502001152L, + 'uuid': 'kkon5B-vnFI-eKHn-I5cG-Hj0C-uGx0-xqZrXI'}, + {'pvname': '/dev/sda2', + 'size': 21470642176L, + 'uuid': 'CyBzhK-cQFl-gWqr-fyWC-A50Y-LMxu-iHiJq4'}] + """ + cmd = ['pvs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'pv_name,pv_size,pv_uuid,vg_name'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of PVs filtered by vgname, if + # provided + pvs = filter(lambda f: vgname is None or vgname in f, + map(lambda v: v.strip(), out.strip('\n').split('\n'))) + + # create a dict based on data retrieved from pvs + return map(lambda l: {'pvname': l[0], + 'size': long(l[1]), + 'uuid': l[2]}, + [fields.split() for fields in pvs]) diff --git a/src/wok/plugins/kimchi/i18n.py b/src/wok/plugins/kimchi/i18n.py index e9be6af..cc55a3a 100644 --- a/src/wok/plugins/kimchi/i18n.py +++ b/src/wok/plugins/kimchi/i18n.py @@ -34,6 +34,7 @@ messages = { "KCHDISKS0001E": _("Error while getting block devices. Details: %(err)s"), "KCHDISKS0002E": _("Error while getting block device information for %(device)s."), + "KCHDISKS0003E": _("Unable to retrieve LVM information. Details: %(err)s"), "KCHDL0001E": _("Unable to find distro file: %(filename)s"), "KCHDL0002E": _("Unable to parse distro file: %(filename)s. Make sure, it is a JSON file."), @@ -334,4 +335,6 @@ messages = { "KCHCPUINF0002E": _("Invalid vCPU/topology combination."), "KCHCPUINF0003E": _("This host (or current configuration) does not allow CPU topology."), + "KCHLVMS0001E": _("Invalid volume group name parameter: %(name)s."), + } diff --git a/src/wok/plugins/kimchi/model/host.py b/src/wok/plugins/kimchi/model/host.py index 9b1fc32..6705ae7 100644 --- a/src/wok/plugins/kimchi/model/host.py +++ b/src/wok/plugins/kimchi/model/host.py @@ -293,6 +293,36 @@ class HostStatsModel(object): self.host_stats['net_sent_bytes'].append(sent_bytes) +class VolumeGroupsModel(object): + def __init__(self, **kargs): + pass + + def get_list(self): + if not disks.vgs(): + return [] + #return [vg['vgname'] for vg in disks.vgs() if vg['free'] > 0L] + return [vg['vgname'] for vg in disks.vgs()] + + +class VolumeGroupModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + def _format(vg): + return {'vgname': vg['vgname'], + 'size': vg['size'], + 'free': vg['free'], + 'pvs': [pv['pvname'] for pv in disks.pvs(vg['vgname'])], + 'lvs': [lv['lvname'] for lv in disks.lvs(vg['vgname'])]} + + vgs = [_format(vg) for vg in disks.vgs() if vg['vgname'] == name] + if not vgs: + raise InvalidParameter("KCHLVMS0001E", {'name': name}) + + return vgs[0] + + class HostStatsHistoryModel(object): def __init__(self, **kargs): self.history = HostStatsModel(**kargs) diff --git a/src/wok/plugins/kimchi/model/storagepools.py b/src/wok/plugins/kimchi/model/storagepools.py index db68252..07a9524 100644 --- a/src/wok/plugins/kimchi/model/storagepools.py +++ b/src/wok/plugins/kimchi/model/storagepools.py @@ -144,6 +144,11 @@ class StoragePoolsModel(object): def create(self, params): task_id = None conn = self.conn.get() + + use_existing_vg = False + if 'vgselect' in params and params['vgselect'] == 'existingvg': + use_existing_vg = True + try: name = params['name'] if name == ISO_POOL_NAME: @@ -152,7 +157,7 @@ class StoragePoolsModel(object): # The user may want to create a logical pool with the same name # used before but a volume group will already exist with this name # So check the volume group does not exist to create the pool - if params['type'] == 'logical': + if params['type'] == 'logical' and not use_existing_vg: vgdisplay_cmd = ['vgdisplay', name.encode('utf-8')] output, error, returncode = run_command(vgdisplay_cmd) # From vgdisplay error codes: -- 1.9.1

On 22/10/2015 22:52, Jose Ricardo Ziviani wrote:
- This commit implements the backend for users to create a storage pool using an existing backend in the system.
Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- src/wok/plugins/kimchi/control/host.py | 22 +++++ src/wok/plugins/kimchi/disks.py | 123 +++++++++++++++++++++++++++ src/wok/plugins/kimchi/i18n.py | 3 + src/wok/plugins/kimchi/model/host.py | 30 +++++++ src/wok/plugins/kimchi/model/storagepools.py | 7 +- 5 files changed, 184 insertions(+), 1 deletion(-)
diff --git a/src/wok/plugins/kimchi/control/host.py b/src/wok/plugins/kimchi/control/host.py index 9fe4c0a..1fa236e 100644 --- a/src/wok/plugins/kimchi/control/host.py +++ b/src/wok/plugins/kimchi/control/host.py @@ -42,6 +42,28 @@ class Host(Resource): self.swupdate = self.generate_action_handler_task('swupdate') self.swupdateprogress = SoftwareUpdateProgress(self.model) self.cpuinfo = CPUInfo(self.model) + self.vgs = VolumeGroups(self.model) + + @property + def data(self): + return self.info + + +class VolumeGroups(Collection): + def __init__(self, model): + super(VolumeGroups, self).__init__(model) + self.role_key = 'host' + self.uri_fmt = "/host/vgs" + self.admin_methods = ['GET'] + self.resource = VolumeGroup + + +class VolumeGroup(Resource): + def __init__(self, model, id=None): + super(VolumeGroup, self).__init__(model, id) + self.role_key = 'host' + self.uri_fmt = "/host/vgs/%s" + self.admin_methods = ['GET']
@property def data(self): diff --git a/src/wok/plugins/kimchi/disks.py b/src/wok/plugins/kimchi/disks.py index eb40e3a..30a52b9 100644 --- a/src/wok/plugins/kimchi/disks.py +++ b/src/wok/plugins/kimchi/disks.py @@ -194,3 +194,126 @@ def get_partition_details(name): dev['path'] = dev_path dev['name'] = name return dev + + +def vgs(): + """ + lists all volume groups in the system. All size units are in bytes. + + [{'vgname': 'vgtest', 'size': 999653638144L, 'free': 0}] + """ + cmd = ['vgs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'vg_name,vg_size,vg_free'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of VGs + vgs = map(lambda v: v.strip(), out.strip('\n').split('\n')) + + # create a dict based on data retrieved from vgs + return map(lambda l: {'vgname': l[0], + 'size': long(l[1]), + 'free': long(l[2])}, + [fields.split() for fields in vgs]) + + +def lvs(vgname=None): + """ + lists all logical volumes found in the system. It can be filtered by + the volume group. All size units are in bytes. + + [{'lvname': 'lva', 'path': '/dev/vgtest/lva', 'size': 12345L}, + {'lvname': 'lvb', 'path': '/dev/vgtest/lvb', 'size': 12345L}] + """ + cmd = ['lvs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'lv_name,lv_path,lv_size,vg_name'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of LVs filtered by vgname, if + # provided + lvs = filter(lambda f: vgname is None or vgname in f, + map(lambda v: v.strip(), out.strip('\n').split('\n'))) + + # create a dict based on data retrieved from lvs + return map(lambda l: {'lvname': l[0], + 'path': l[1], + 'size': long(l[2])}, + [fields.split() for fields in lvs]) + + +def pvs(vgname=None): + """ + lists all physical volumes in the system. It can be filtered by the + volume group. All size units are in bytes. + + [{'pvname': '/dev/sda3', + 'size': 469502001152L, + 'uuid': 'kkon5B-vnFI-eKHn-I5cG-Hj0C-uGx0-xqZrXI'}, + {'pvname': '/dev/sda2', + 'size': 21470642176L, + 'uuid': 'CyBzhK-cQFl-gWqr-fyWC-A50Y-LMxu-iHiJq4'}] + """ + cmd = ['pvs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'pv_name,pv_size,pv_uuid,vg_name'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of PVs filtered by vgname, if + # provided + pvs = filter(lambda f: vgname is None or vgname in f, + map(lambda v: v.strip(), out.strip('\n').split('\n'))) + + # create a dict based on data retrieved from pvs + return map(lambda l: {'pvname': l[0], + 'size': long(l[1]), + 'uuid': l[2]}, + [fields.split() for fields in pvs]) diff --git a/src/wok/plugins/kimchi/i18n.py b/src/wok/plugins/kimchi/i18n.py index e9be6af..cc55a3a 100644 --- a/src/wok/plugins/kimchi/i18n.py +++ b/src/wok/plugins/kimchi/i18n.py @@ -34,6 +34,7 @@ messages = {
"KCHDISKS0001E": _("Error while getting block devices. Details: %(err)s"), "KCHDISKS0002E": _("Error while getting block device information for %(device)s."), + "KCHDISKS0003E": _("Unable to retrieve LVM information. Details: %(err)s"),
"KCHDL0001E": _("Unable to find distro file: %(filename)s"), "KCHDL0002E": _("Unable to parse distro file: %(filename)s. Make sure, it is a JSON file."), @@ -334,4 +335,6 @@ messages = { "KCHCPUINF0002E": _("Invalid vCPU/topology combination."), "KCHCPUINF0003E": _("This host (or current configuration) does not allow CPU topology."),
+ "KCHLVMS0001E": _("Invalid volume group name parameter: %(name)s."), + } diff --git a/src/wok/plugins/kimchi/model/host.py b/src/wok/plugins/kimchi/model/host.py index 9b1fc32..6705ae7 100644 --- a/src/wok/plugins/kimchi/model/host.py +++ b/src/wok/plugins/kimchi/model/host.py @@ -293,6 +293,36 @@ class HostStatsModel(object): self.host_stats['net_sent_bytes'].append(sent_bytes)
+class VolumeGroupsModel(object): + def __init__(self, **kargs): + pass + + def get_list(self): + if not disks.vgs(): + return []
+ #return [vg['vgname'] for vg in disks.vgs() if vg['free'] > 0L]
You can remove this line
+ return [vg['vgname'] for vg in disks.vgs()] + + +class VolumeGroupModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + def _format(vg): + return {'vgname': vg['vgname'], + 'size': vg['size'], + 'free': vg['free'], + 'pvs': [pv['pvname'] for pv in disks.pvs(vg['vgname'])], + 'lvs': [lv['lvname'] for lv in disks.lvs(vg['vgname'])]} + + vgs = [_format(vg) for vg in disks.vgs() if vg['vgname'] == name] + if not vgs: + raise InvalidParameter("KCHLVMS0001E", {'name': name}) + + return vgs[0] + + class HostStatsHistoryModel(object): def __init__(self, **kargs): self.history = HostStatsModel(**kargs) diff --git a/src/wok/plugins/kimchi/model/storagepools.py b/src/wok/plugins/kimchi/model/storagepools.py index db68252..07a9524 100644 --- a/src/wok/plugins/kimchi/model/storagepools.py +++ b/src/wok/plugins/kimchi/model/storagepools.py @@ -144,6 +144,11 @@ class StoragePoolsModel(object): def create(self, params): task_id = None conn = self.conn.get()
+ + use_existing_vg = False + if 'vgselect' in params and params['vgselect'] == 'existingvg': + use_existing_vg = True +
I'd suggest to use a boolean parameter, for example, 'from_vg' to represent that. Also, from the libvirt documentation (https://libvirt.org/storage.html#StorageBackendLogical), the user just needs to specify the VG name to create the pool from it. So does it mean we can not create a logical pool from an existing VG with a different name?
try: name = params['name'] if name == ISO_POOL_NAME: @@ -152,7 +157,7 @@ class StoragePoolsModel(object): # The user may want to create a logical pool with the same name # used before but a volume group will already exist with this name # So check the volume group does not exist to create the pool - if params['type'] == 'logical': + if params['type'] == 'logical' and not use_existing_vg: vgdisplay_cmd = ['vgdisplay', name.encode('utf-8')] output, error, returncode = run_command(vgdisplay_cmd) # From vgdisplay error codes:

Also need to update the docs/API.md to reflect those changes On 22/10/2015 22:52, Jose Ricardo Ziviani wrote:
- This commit implements the backend for users to create a storage pool using an existing backend in the system.
Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- src/wok/plugins/kimchi/control/host.py | 22 +++++ src/wok/plugins/kimchi/disks.py | 123 +++++++++++++++++++++++++++ src/wok/plugins/kimchi/i18n.py | 3 + src/wok/plugins/kimchi/model/host.py | 30 +++++++ src/wok/plugins/kimchi/model/storagepools.py | 7 +- 5 files changed, 184 insertions(+), 1 deletion(-)
diff --git a/src/wok/plugins/kimchi/control/host.py b/src/wok/plugins/kimchi/control/host.py index 9fe4c0a..1fa236e 100644 --- a/src/wok/plugins/kimchi/control/host.py +++ b/src/wok/plugins/kimchi/control/host.py @@ -42,6 +42,28 @@ class Host(Resource): self.swupdate = self.generate_action_handler_task('swupdate') self.swupdateprogress = SoftwareUpdateProgress(self.model) self.cpuinfo = CPUInfo(self.model) + self.vgs = VolumeGroups(self.model) + + @property + def data(self): + return self.info + + +class VolumeGroups(Collection): + def __init__(self, model): + super(VolumeGroups, self).__init__(model) + self.role_key = 'host' + self.uri_fmt = "/host/vgs" + self.admin_methods = ['GET'] + self.resource = VolumeGroup + + +class VolumeGroup(Resource): + def __init__(self, model, id=None): + super(VolumeGroup, self).__init__(model, id) + self.role_key = 'host' + self.uri_fmt = "/host/vgs/%s" + self.admin_methods = ['GET']
@property def data(self): diff --git a/src/wok/plugins/kimchi/disks.py b/src/wok/plugins/kimchi/disks.py index eb40e3a..30a52b9 100644 --- a/src/wok/plugins/kimchi/disks.py +++ b/src/wok/plugins/kimchi/disks.py @@ -194,3 +194,126 @@ def get_partition_details(name): dev['path'] = dev_path dev['name'] = name return dev + + +def vgs(): + """ + lists all volume groups in the system. All size units are in bytes. + + [{'vgname': 'vgtest', 'size': 999653638144L, 'free': 0}] + """ + cmd = ['vgs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'vg_name,vg_size,vg_free'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of VGs + vgs = map(lambda v: v.strip(), out.strip('\n').split('\n')) + + # create a dict based on data retrieved from vgs + return map(lambda l: {'vgname': l[0], + 'size': long(l[1]), + 'free': long(l[2])}, + [fields.split() for fields in vgs]) + + +def lvs(vgname=None): + """ + lists all logical volumes found in the system. It can be filtered by + the volume group. All size units are in bytes. + + [{'lvname': 'lva', 'path': '/dev/vgtest/lva', 'size': 12345L}, + {'lvname': 'lvb', 'path': '/dev/vgtest/lvb', 'size': 12345L}] + """ + cmd = ['lvs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'lv_name,lv_path,lv_size,vg_name'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of LVs filtered by vgname, if + # provided + lvs = filter(lambda f: vgname is None or vgname in f, + map(lambda v: v.strip(), out.strip('\n').split('\n'))) + + # create a dict based on data retrieved from lvs + return map(lambda l: {'lvname': l[0], + 'path': l[1], + 'size': long(l[2])}, + [fields.split() for fields in lvs]) + + +def pvs(vgname=None): + """ + lists all physical volumes in the system. It can be filtered by the + volume group. All size units are in bytes. + + [{'pvname': '/dev/sda3', + 'size': 469502001152L, + 'uuid': 'kkon5B-vnFI-eKHn-I5cG-Hj0C-uGx0-xqZrXI'}, + {'pvname': '/dev/sda2', + 'size': 21470642176L, + 'uuid': 'CyBzhK-cQFl-gWqr-fyWC-A50Y-LMxu-iHiJq4'}] + """ + cmd = ['pvs', + '--units', + 'b', + '--nosuffix', + '--noheading', + '--unbuffered', + '--options', + 'pv_name,pv_size,pv_uuid,vg_name'] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = proc.communicate() + + if proc.returncode != 0: + raise OperationFailed("KCHDISKS0003E", {'err': err}) + + if not out: + return [] + + # remove blank spaces and create a list of PVs filtered by vgname, if + # provided + pvs = filter(lambda f: vgname is None or vgname in f, + map(lambda v: v.strip(), out.strip('\n').split('\n'))) + + # create a dict based on data retrieved from pvs + return map(lambda l: {'pvname': l[0], + 'size': long(l[1]), + 'uuid': l[2]}, + [fields.split() for fields in pvs]) diff --git a/src/wok/plugins/kimchi/i18n.py b/src/wok/plugins/kimchi/i18n.py index e9be6af..cc55a3a 100644 --- a/src/wok/plugins/kimchi/i18n.py +++ b/src/wok/plugins/kimchi/i18n.py @@ -34,6 +34,7 @@ messages = {
"KCHDISKS0001E": _("Error while getting block devices. Details: %(err)s"), "KCHDISKS0002E": _("Error while getting block device information for %(device)s."), + "KCHDISKS0003E": _("Unable to retrieve LVM information. Details: %(err)s"),
"KCHDL0001E": _("Unable to find distro file: %(filename)s"), "KCHDL0002E": _("Unable to parse distro file: %(filename)s. Make sure, it is a JSON file."), @@ -334,4 +335,6 @@ messages = { "KCHCPUINF0002E": _("Invalid vCPU/topology combination."), "KCHCPUINF0003E": _("This host (or current configuration) does not allow CPU topology."),
+ "KCHLVMS0001E": _("Invalid volume group name parameter: %(name)s."), + } diff --git a/src/wok/plugins/kimchi/model/host.py b/src/wok/plugins/kimchi/model/host.py index 9b1fc32..6705ae7 100644 --- a/src/wok/plugins/kimchi/model/host.py +++ b/src/wok/plugins/kimchi/model/host.py @@ -293,6 +293,36 @@ class HostStatsModel(object): self.host_stats['net_sent_bytes'].append(sent_bytes)
+class VolumeGroupsModel(object): + def __init__(self, **kargs): + pass + + def get_list(self): + if not disks.vgs(): + return [] + #return [vg['vgname'] for vg in disks.vgs() if vg['free'] > 0L] + return [vg['vgname'] for vg in disks.vgs()] + + +class VolumeGroupModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + def _format(vg): + return {'vgname': vg['vgname'], + 'size': vg['size'], + 'free': vg['free'], + 'pvs': [pv['pvname'] for pv in disks.pvs(vg['vgname'])], + 'lvs': [lv['lvname'] for lv in disks.lvs(vg['vgname'])]} + + vgs = [_format(vg) for vg in disks.vgs() if vg['vgname'] == name] + if not vgs: + raise InvalidParameter("KCHLVMS0001E", {'name': name}) + + return vgs[0] + + class HostStatsHistoryModel(object): def __init__(self, **kargs): self.history = HostStatsModel(**kargs) diff --git a/src/wok/plugins/kimchi/model/storagepools.py b/src/wok/plugins/kimchi/model/storagepools.py index db68252..07a9524 100644 --- a/src/wok/plugins/kimchi/model/storagepools.py +++ b/src/wok/plugins/kimchi/model/storagepools.py @@ -144,6 +144,11 @@ class StoragePoolsModel(object): def create(self, params): task_id = None conn = self.conn.get() + + use_existing_vg = False + if 'vgselect' in params and params['vgselect'] == 'existingvg': + use_existing_vg = True + try: name = params['name'] if name == ISO_POOL_NAME: @@ -152,7 +157,7 @@ class StoragePoolsModel(object): # The user may want to create a logical pool with the same name # used before but a volume group will already exist with this name # So check the volume group does not exist to create the pool - if params['type'] == 'logical': + if params['type'] == 'logical' and not use_existing_vg: vgdisplay_cmd = ['vgdisplay', name.encode('utf-8')] output, error, returncode = run_command(vgdisplay_cmd) # From vgdisplay error codes:

Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- src/wok/plugins/kimchi/API.json | 5 ++ .../kimchi/ui/css/theme-default/storage.css | 22 ++++++++ src/wok/plugins/kimchi/ui/js/src/kimchi.api.js | 11 ++++ .../ui/js/src/kimchi.storagepool_add_main.js | 58 ++++++++++++++++++++-- .../kimchi/ui/pages/storagepool-add.html.tmpl | 38 +++++++++++++- 5 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/wok/plugins/kimchi/API.json b/src/wok/plugins/kimchi/API.json index fc1d2dd..ce09039 100644 --- a/src/wok/plugins/kimchi/API.json +++ b/src/wok/plugins/kimchi/API.json @@ -165,6 +165,11 @@ "error": "KCHPOOL0025E" } } + }, + "vgselect": { + "description": "Use an existing VG or create a new one", + "type": "string", + "error": "KCHPOOL0026E" } } } diff --git a/src/wok/plugins/kimchi/ui/css/theme-default/storage.css b/src/wok/plugins/kimchi/ui/css/theme-default/storage.css index 88447b5..a5952ae 100644 --- a/src/wok/plugins/kimchi/ui/css/theme-default/storage.css +++ b/src/wok/plugins/kimchi/ui/css/theme-default/storage.css @@ -422,6 +422,28 @@ white-space: nowrap; } +.existing-groups { + padding-left:13px; +} + +.form-section .field { + font-size: 13px; + font-weight: lighter; +} + +.existing-groups>table { + width: 300px; + padding: 13px; + margin-left: 20px; + font-size: 13px; + font-weight: lighter; +} + +.existing-groups>table>thead { + text-align: left; + background-color: rgb(0, 138, 191); +} + .storage-type-wrapper-controls { width: 300px; display: inline-block; diff --git a/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js b/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js index a16c95e..bd60bbb 100644 --- a/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js +++ b/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js @@ -745,6 +745,17 @@ var kimchi = { }); }, + listExistingVGs : function(suc, err) { + wok.requestJSON({ + url : 'plugins/kimchi/host/vgs', + type: 'GET', + contentType : 'aplication/json', + dataType : 'json', + success : suc, + error : err + }); + }, + getStorageServers: function(type, suc, err) { var url = 'plugins/kimchi/storageservers?_target_type=' + type; wok.requestJSON({ diff --git a/src/wok/plugins/kimchi/ui/js/src/kimchi.storagepool_add_main.js b/src/wok/plugins/kimchi/ui/js/src/kimchi.storagepool_add_main.js index 8c27539..9cfa446 100644 --- a/src/wok/plugins/kimchi/ui/js/src/kimchi.storagepool_add_main.js +++ b/src/wok/plugins/kimchi/ui/js/src/kimchi.storagepool_add_main.js @@ -141,6 +141,32 @@ kimchi.initStorageAddPage = function() { $('.host-partition').addClass('text-help'); }); + kimchi.listExistingVGs(function(data) { + if (data.length == 0) { + $('.existing-groups').html(i18n['KCHPOOL6011M']); + $('.existing-groups').addClass('text-help'); + return; + } + var deviceHtml = $('#vgTmpl').html(); + var listHtml = ''; + $.each(data, function(index, value) { + value.size /= Math.pow(1024, 3); + value.size = value.size.toFixed() + " GiB"; + value.free /= Math.pow(1024, 3); + value.free = value.free.toFixed() + " GiB"; + listHtml += wok.substitute(deviceHtml, value); + }); + $('#vgrows').html(listHtml); + + $('input[type=radio][name=devices]').change(function() { + var vgdev=$('input[type=radio][name=devices]:checked').val(); + $('#poolId').val(vgdev); + }); + }, function(err) { + $('.existing-groups').html(i18n['KCHPOOL6013M'] + '<br/>(' + err.responseJSON.reason + ')'); + $('.existing-groups').addClass('text-help'); + }); + kimchi.getHostFCDevices(function(data){ if(data.length>0){ for(var i=0;i<data.length;i++){ @@ -235,6 +261,22 @@ kimchi.initStorageAddPage = function() { $(value).addClass('tmpl-html'); } }); + + $("#poolId").removeAttr("disabled"); + if (selectType == 'logical') { + $('input[type=radio][name=vgselect]').attr('checked', 'checked'); + } + }); + $('input[type=radio][name=vgselect]').change(function() { + if ($(this).val() == 'existingvg') { + $('.existing-groups').removeClass('tmpl-html'); + $('.host-partition').addClass('tmpl-html'); + $("#poolId").attr("disabled", "disabled"); + } else { + $('.existing-groups').addClass('tmpl-html'); + $('.host-partition').removeClass('tmpl-html'); + $("#poolId").removeAttr("disabled"); + } }); $('#authId').click(function() { if ($(this).prop("checked")) { @@ -274,7 +316,11 @@ kimchi.inputsNotBlank = function() { if (!$('#iscsiserverId').val()) return false; if (!$('#iscsiTargetId').val()) return false; } else if (poolType === "logical") { - if ($("input[name=devices]:checked").length === 0) + var vgtype=$('input[type=radio][name=vgselect]:checked').val(); + if (vgtype == 'newvg' && $('input[name=devices]:checked').length === 0) + return false; + else if (vgtype == 'existingvg' && + $('input[type=radio][name=devices]:checked').length !== 1) return false; } return true; @@ -336,12 +382,16 @@ kimchi.validateServer = function(serverField) { }; kimchi.validateLogicalForm = function () { - if ($("input[name=devices]:checked").length === 0) { + var vgtype=$('input[type=radio][name=vgselect]:checked').val(); + if (vgtype == 'newvg' && $("input[name=devices]:checked").length === 0) { + wok.message.error.code('KCHPOOL6006E'); + return false; + } else if (vgtype == 'existingvg' && + $('input[type=radio][name=devices]:checked').length !== 1) { wok.message.error.code('KCHPOOL6006E'); return false; - } else { - return true; } + return true; }; kimchi.addPool = function(event) { diff --git a/src/wok/plugins/kimchi/ui/pages/storagepool-add.html.tmpl b/src/wok/plugins/kimchi/ui/pages/storagepool-add.html.tmpl index a697af5..23e5877 100644 --- a/src/wok/plugins/kimchi/ui/pages/storagepool-add.html.tmpl +++ b/src/wok/plugins/kimchi/ui/pages/storagepool-add.html.tmpl @@ -99,8 +99,15 @@ </section> </div> <div class="logical-section tmpl-html"> + <section class="form-section"> + <h2>3. Create a new volume group or use an existing one?</h2> + <div class="field"> + <input type="radio" name="vgselect" value="newvg" checked="checked">New VG</input> + <input type="radio" name="vgselect" value="existingvg">Existing VG</input> + </div> + </section> <section class="form-section storageType"> - <h2>3. $_("Device path")</h2> + <h2>4. $_("Device path")</h2> <div class="host-partition"> <div class="icon-info-circled light-grey c1 help-inline"></div> <p class="text-help help-inline"> @@ -108,6 +115,27 @@ <img src = "plugins/kimchi/images/theme-default/loading.gif" /> </p> </div> + <div class="existing-groups tmpl-html"> + <div class="icon-info-circled light-grey c1 help-inline"></div> + <table class="volume-groups"> + <thead> + <tr> + <th></th> + <th>Volume Group</th> + <th>Size</th> + <th>Free space</th> + </tr> + </thead> + <tbody id="vgrows"> + <tr class="text-help help-inline"> + <td colspan="4"> + $_("Looking for available partitions ...") + <img src = "plugins/kimchi/images/theme-default/loading.gif" /> + </td> + </tr> + </tbody> + </table> + </div> </section> </div> <div class="iscsi-section tmpl-html"> @@ -182,5 +210,13 @@ <label for="{name}">{path}</label> </div> </script> + <script id="vgTmpl" type="html/text"> + <tr> + <td><input type="radio" name="devices" value="{vgname}"/></td> + <td>{vgname}</td> + <td>{size}</td> + <td>{free}</td> + </tr> + </script> </body> </html> -- 1.9.1

On 22/10/2015 22:52, Jose Ricardo Ziviani wrote:
Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- src/wok/plugins/kimchi/API.json | 5 ++ .../kimchi/ui/css/theme-default/storage.css | 22 ++++++++ src/wok/plugins/kimchi/ui/js/src/kimchi.api.js | 11 ++++ .../ui/js/src/kimchi.storagepool_add_main.js | 58 ++++++++++++++++++++-- .../kimchi/ui/pages/storagepool-add.html.tmpl | 38 +++++++++++++- 5 files changed, 129 insertions(+), 5 deletions(-)
diff --git a/src/wok/plugins/kimchi/API.json b/src/wok/plugins/kimchi/API.json index fc1d2dd..ce09039 100644 --- a/src/wok/plugins/kimchi/API.json +++ b/src/wok/plugins/kimchi/API.json @@ -165,6 +165,11 @@ "error": "KCHPOOL0025E" } } + }, + "vgselect": { + "description": "Use an existing VG or create a new one", + "type": "string", + "error": "KCHPOOL0026E"
It is better to use a boolean parameter here.
} } } diff --git a/src/wok/plugins/kimchi/ui/css/theme-default/storage.css b/src/wok/plugins/kimchi/ui/css/theme-default/storage.css index 88447b5..a5952ae 100644 --- a/src/wok/plugins/kimchi/ui/css/theme-default/storage.css +++ b/src/wok/plugins/kimchi/ui/css/theme-default/storage.css @@ -422,6 +422,28 @@ white-space: nowrap; }
+.existing-groups { + padding-left:13px; +} + +.form-section .field { + font-size: 13px; + font-weight: lighter; +} + +.existing-groups>table { + width: 300px; + padding: 13px; + margin-left: 20px; + font-size: 13px; + font-weight: lighter; +} + +.existing-groups>table>thead { + text-align: left; + background-color: rgb(0, 138, 191); +} + .storage-type-wrapper-controls { width: 300px; display: inline-block; diff --git a/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js b/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js index a16c95e..bd60bbb 100644 --- a/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js +++ b/src/wok/plugins/kimchi/ui/js/src/kimchi.api.js @@ -745,6 +745,17 @@ var kimchi = { }); },
+ listExistingVGs : function(suc, err) { + wok.requestJSON({ + url : 'plugins/kimchi/host/vgs', + type: 'GET', + contentType : 'aplication/json', + dataType : 'json', + success : suc, + error : err + }); + }, + getStorageServers: function(type, suc, err) { var url = 'plugins/kimchi/storageservers?_target_type=' + type; wok.requestJSON({ diff --git a/src/wok/plugins/kimchi/ui/js/src/kimchi.storagepool_add_main.js b/src/wok/plugins/kimchi/ui/js/src/kimchi.storagepool_add_main.js index 8c27539..9cfa446 100644 --- a/src/wok/plugins/kimchi/ui/js/src/kimchi.storagepool_add_main.js +++ b/src/wok/plugins/kimchi/ui/js/src/kimchi.storagepool_add_main.js @@ -141,6 +141,32 @@ kimchi.initStorageAddPage = function() { $('.host-partition').addClass('text-help'); });
+ kimchi.listExistingVGs(function(data) { + if (data.length == 0) { + $('.existing-groups').html(i18n['KCHPOOL6011M']); + $('.existing-groups').addClass('text-help'); + return; + } + var deviceHtml = $('#vgTmpl').html(); + var listHtml = ''; + $.each(data, function(index, value) { + value.size /= Math.pow(1024, 3); + value.size = value.size.toFixed() + " GiB"; + value.free /= Math.pow(1024, 3); + value.free = value.free.toFixed() + " GiB"; + listHtml += wok.substitute(deviceHtml, value); + }); + $('#vgrows').html(listHtml); + + $('input[type=radio][name=devices]').change(function() { + var vgdev=$('input[type=radio][name=devices]:checked').val(); + $('#poolId').val(vgdev); + }); + }, function(err) { + $('.existing-groups').html(i18n['KCHPOOL6013M'] + '<br/>(' + err.responseJSON.reason + ')'); + $('.existing-groups').addClass('text-help'); + }); + kimchi.getHostFCDevices(function(data){ if(data.length>0){ for(var i=0;i<data.length;i++){ @@ -235,6 +261,22 @@ kimchi.initStorageAddPage = function() { $(value).addClass('tmpl-html'); } }); + + $("#poolId").removeAttr("disabled"); + if (selectType == 'logical') { + $('input[type=radio][name=vgselect]').attr('checked', 'checked'); + } + }); + $('input[type=radio][name=vgselect]').change(function() { + if ($(this).val() == 'existingvg') { + $('.existing-groups').removeClass('tmpl-html'); + $('.host-partition').addClass('tmpl-html'); + $("#poolId").attr("disabled", "disabled"); + } else { + $('.existing-groups').addClass('tmpl-html'); + $('.host-partition').removeClass('tmpl-html'); + $("#poolId").removeAttr("disabled"); + } }); $('#authId').click(function() { if ($(this).prop("checked")) { @@ -274,7 +316,11 @@ kimchi.inputsNotBlank = function() { if (!$('#iscsiserverId').val()) return false; if (!$('#iscsiTargetId').val()) return false; } else if (poolType === "logical") { - if ($("input[name=devices]:checked").length === 0) + var vgtype=$('input[type=radio][name=vgselect]:checked').val(); + if (vgtype == 'newvg' && $('input[name=devices]:checked').length === 0) + return false; + else if (vgtype == 'existingvg' && + $('input[type=radio][name=devices]:checked').length !== 1) return false; } return true; @@ -336,12 +382,16 @@ kimchi.validateServer = function(serverField) { };
kimchi.validateLogicalForm = function () { - if ($("input[name=devices]:checked").length === 0) { + var vgtype=$('input[type=radio][name=vgselect]:checked').val(); + if (vgtype == 'newvg' && $("input[name=devices]:checked").length === 0) { + wok.message.error.code('KCHPOOL6006E'); + return false; + } else if (vgtype == 'existingvg' && + $('input[type=radio][name=devices]:checked').length !== 1) { wok.message.error.code('KCHPOOL6006E'); return false; - } else { - return true; } + return true; };
kimchi.addPool = function(event) { diff --git a/src/wok/plugins/kimchi/ui/pages/storagepool-add.html.tmpl b/src/wok/plugins/kimchi/ui/pages/storagepool-add.html.tmpl index a697af5..23e5877 100644 --- a/src/wok/plugins/kimchi/ui/pages/storagepool-add.html.tmpl +++ b/src/wok/plugins/kimchi/ui/pages/storagepool-add.html.tmpl @@ -99,8 +99,15 @@ </section> </div> <div class="logical-section tmpl-html"> + <section class="form-section"> + <h2>3. Create a new volume group or use an existing one?</h2> + <div class="field"> + <input type="radio" name="vgselect" value="newvg" checked="checked">New VG</input> + <input type="radio" name="vgselect" value="existingvg">Existing VG</input> + </div> + </section> <section class="form-section storageType"> - <h2>3. $_("Device path")</h2> + <h2>4. $_("Device path")</h2> <div class="host-partition"> <div class="icon-info-circled light-grey c1 help-inline"></div> <p class="text-help help-inline"> @@ -108,6 +115,27 @@ <img src = "plugins/kimchi/images/theme-default/loading.gif" /> </p> </div> + <div class="existing-groups tmpl-html"> + <div class="icon-info-circled light-grey c1 help-inline"></div> + <table class="volume-groups"> + <thead> + <tr> + <th></th> + <th>Volume Group</th> + <th>Size</th> + <th>Free space</th> + </tr> + </thead> + <tbody id="vgrows"> + <tr class="text-help help-inline"> + <td colspan="4"> + $_("Looking for available partitions ...") + <img src = "plugins/kimchi/images/theme-default/loading.gif" /> + </td> + </tr> + </tbody> + </table> + </div> </section> </div> <div class="iscsi-section tmpl-html"> @@ -182,5 +210,13 @@ <label for="{name}">{path}</label> </div> </script> + <script id="vgTmpl" type="html/text"> + <tr> + <td><input type="radio" name="devices" value="{vgname}"/></td> + <td>{vgname}</td> + <td>{size}</td> + <td>{free}</td> + </tr> + </script> </body> </html>

Hi Ziviani! Thanks for sending out those patches! I will rebase them on top of master to test and apply the comments I made. I will also add tests before sending the patches back for review. I will ask Socorro to help me on UI. Regards, Aline Manera On 22/10/2015 22:52, Jose Ricardo Ziviani wrote:
Team, this work is not completed. I'm sending it to ML anyway because probably someone else will continue on it.
What it has to be done: - implement test; - finish the frontend part: - storage pool name textfield cannot be edited if user select a VG (that vgname should populate the storage pool name automatically). - fix any UI bug. - create error messages accordingly (error codes used in frontend are mockups); - Add i18n for tmpl file.
The backend looks ok but I made some quick tests during the development.
I'll be reading my e-mails during the residency so I'm pretty sure we can work together.
Thanks
Jose Ricardo Ziviani (2): Implement the backend to use existing VG as storage pool Implement the frontend to use existing VG as storage pool
src/wok/plugins/kimchi/API.json | 5 + src/wok/plugins/kimchi/control/host.py | 22 ++++ src/wok/plugins/kimchi/disks.py | 123 +++++++++++++++++++++ src/wok/plugins/kimchi/i18n.py | 3 + src/wok/plugins/kimchi/model/host.py | 30 +++++ src/wok/plugins/kimchi/model/storagepools.py | 7 +- .../kimchi/ui/css/theme-default/storage.css | 22 ++++ src/wok/plugins/kimchi/ui/js/src/kimchi.api.js | 11 ++ .../ui/js/src/kimchi.storagepool_add_main.js | 58 +++++++++- .../kimchi/ui/pages/storagepool-add.html.tmpl | 38 ++++++- 10 files changed, 313 insertions(+), 6 deletions(-)
participants (2)
-
Aline Manera
-
Jose Ricardo Ziviani