Verified Commit 4cfa7b1e authored by Loïc Dachary's avatar Loïc Dachary Committed by Loïc Dachary

enough: implement volume resize cli

Fixes: #164
parent 0327e9a3
......@@ -253,7 +253,7 @@ Provisioning
A volume can be created and attached to the host. It can be resized at
a later time, when more space is needed. For instance, before creating
`weblate-host`, the desired volume size and name can be set in the
`~/.enough/example.com/inventory/host_vars/weblate-host/network.yml`
`~/.enough/example.com/inventory/host_vars/weblate-host/provision.yml`
file like so:
.. code::
......@@ -295,6 +295,45 @@ variables like so:
encrypted_volume_for_docker: false
encrypted_volume_for_snap: false
Resizing
++++++++
The size of a volume can be increased (but never decreased) by
modifying the value from (for instance) 10GB
.. code::
---
openstack_volumes:
- name: weblate-volume
size: 10
to 20GB
.. code::
---
openstack_volumes:
- name: weblate-volume
size: 20
The resize operation is done with the following command (the host will
be rebooted). If the volume already has the desired size, the command
will do nothing.
.. code::
$ enough --domain example.com volume resize weblate-host weblate-volume
If the volume is mounted as an encrypted partition, it should then be
extended to use the additional disk space. There is no need to unmount
the partition.
.. code::
$ enough --domain example.com ssh weblate-host -- sudo cryptsetup resize --key-file=/etc/cryptsetup/keyfile spare
$ enough --domain example.com ssh weblate-host -- sudo resize2fs /dev/mapper/spare
Services
~~~~~~~~
......
from cliff.command import Command
from enough import settings
from enough.common import Enough
from enough.common import options
class Resize(Command):
"Resize a volume"
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
parser.add_argument('host')
parser.add_argument('volume')
return options.set_options(parser)
def take_action(self, parsed_args):
args = vars(self.app.options)
args.update(vars(parsed_args))
e = Enough(settings.CONFIG_DIR, settings.SHARE_DIR, **args)
e.volume_resize(args['host'], args['volume'])
......@@ -255,3 +255,26 @@ class Enough(object):
host = clone.create_service_matching_snapshot(snapshot)
clone.openstack.replace_volume(host, snapshot, delete_volume=True)
return clone
class VolumeResizeUndefined(Exception):
pass
class VolumeResizeNoSize(Exception):
pass
def volume_resize(self, host, volume):
d = self.heat.get_stack_definitions()[host]
volumes = d.get('openstack_volumes')
if not volumes:
raise Enough.VolumeResizeUndefined(f'no openstack_volumes variable is set for {host}')
volume_size = None
for info in volumes:
if info['name'] == volume:
volume_size = info.get('size')
break
if volume_size is None:
raise Enough.VolumeResizeNoSize(f'no size found for {volume} in the openstack_volumes '
f'variable ({volumes}) for {host}')
r = self.openstack.volume_resize(host, volume, int(volume_size))
self.openstack.wait_for_ssh(d['ansible_host'], d['ansible_port'])
return r
......@@ -313,6 +313,18 @@ class OpenStackShutoff(Exception):
pass
class OpenStackVolumeResizeMissing(Exception):
pass
class OpenStackVolumeResizeMismatch(Exception):
pass
class OpenStackVolumeResizeShrink(Exception):
pass
class OpenStack(OpenStackBase):
def __init__(self, config_dir, **kwargs):
......@@ -515,6 +527,33 @@ class OpenStack(OpenStackBase):
self.o.server.add.volume(host, volume_id)
self.o.server.start(host)
def volume_resize(self, host, volume, size):
attached = self.o.server.show(
'--format=value', '-c', 'volumes_attached', host).stdout.strip()
if len(attached) == 0:
raise OpenStackVolumeResizeMissing(
f'{host} is not attached {volume}, it is not attached to any volume')
current_volume_id = re.sub("id='(.*)'", "\\1", attached.decode('utf-8'))
current_volume = self.o.volume.show(
'--format=value', '-c', 'name', current_volume_id).strip()
if current_volume != volume:
raise OpenStackVolumeResizeMismatch(
f'{host} is not attached {volume}, it is attached to {current_volume}')
current_size = int(self.o.volume.show('-c', 'size', '--format=value', volume).strip())
if current_size == size:
log.info(f'resize of {host} volume {volume} is not necessary, it already is {size}GB')
return False
if current_size > size:
raise OpenStackVolumeResizeShrink(
f'{host} {volume} is {current_size}GB and cannot be shrinked to {size}GB')
self.o.server.stop(host)
self.server_wait_shutoff(host)
self.o.server.remove.volume(host, volume)
self.o.volume.set('--size', size, volume)
self.o.server.add.volume(host, volume)
self.o.server.start(host)
return True
def region_empty(self):
volumes = self.o.volume.list()
servers = self.o.server.list()
......
......@@ -78,6 +78,7 @@ enough.cli =
service_create = enough.cli.service:Create
host_create = enough.cli.host:Create
host_delete = enough.cli.host:Delete
volume_resize = enough.cli.volume:Resize
backup_restore = enough.cli.backup:Restore
backup_clone_volume = enough.cli.backup:CloneVolume
......
......@@ -6,7 +6,7 @@ import yaml
from enough import settings
from enough.common import Enough
from enough.common.openstack import OpenStack, Stack
from enough.common.openstack import OpenStackBase, OpenStack
@pytest.mark.skipif('SKIP_OPENSTACK_INTEGRATION_TESTS' in os.environ,
......@@ -235,7 +235,7 @@ def test_restore_remote(tmpdir):
clone = original.restore_remote('test.com', 'clone', True, snapshot)
assert 'sample-volume' in clone.openstack.o.volume.list()
hosts = clone.hosts.load()
Stack.wait_for_ssh(hosts.get_ip('sample-host'), hosts.get_port('sample-host'))
OpenStackBase.wait_for_ssh(hosts.get_ip('sample-host'), hosts.get_port('sample-host'))
assert sh.ssh('-oStrictHostKeyChecking=no',
'-i', clone.dotenough.private_key(), f'debian@{ip}',
'test', '-e', '/srv/STONE').exit_code == 0
......@@ -266,7 +266,7 @@ def test_restore_local(tmpdir):
assert e == e.restore_local(snapshot)
assert 'sample-volume' in e.openstack.o.volume.list()
Stack.wait_for_ssh(e.hosts.get_ip('sample-host'), e.hosts.get_port('sample-host'))
OpenStackBase.wait_for_ssh(e.hosts.get_ip('sample-host'), e.hosts.get_port('sample-host'))
assert sh.ssh('-oStrictHostKeyChecking=no',
'-i', e.dotenough.private_key(), f'debian@{ip}',
'test', '-e', '/srv/STONE').exit_code == 0
......@@ -321,3 +321,22 @@ def test_host_from_snapshot(tmpdir):
host = 'sample-host'
snapshot = f'2010-07-08-sample-volume'
assert e.host_from_snapshot(snapshot) == host
@pytest.mark.skipif('SKIP_OPENSTACK_INTEGRATION_TESTS' in os.environ,
reason='skip integration test')
def test_volume_resize(tmpdir):
try:
enough = create_enough(tmpdir, 'resize')
enough.set_args(name='sample', playbook='enough-playbook.yml')
enough.service.create_or_update()
# False because the size does not change
assert enough.volume_resize('sample-host', 'sample-volume') is False
with pytest.raises(Enough.VolumeResizeUndefined):
enough.volume_resize('other-host', 'other-volume')
with pytest.raises(Enough.VolumeResizeNoSize):
enough.volume_resize('nosize-host', 'nosize-volume')
finally:
o = OpenStack(settings.CONFIG_DIR)
# comment out the following line to re-use the content of the regions and save time
o.destroy_everything(None)
---
- name: do nothing
hosts: sample-host
become: true
tasks:
- debug:
msg: "NOTHING"
---
openstack_volumes:
- name: sample-volume
size: 1
nosize-service-group:
hosts:
nosize-host:
nosize-service-hosts:
children:
nosize-service-group:
other-service-group:
hosts:
other-host:
other-service-hosts:
children:
other-service-group:
sample-service-group:
hosts:
sample-host:
sample-service-hosts:
children:
sample-service-group:
......@@ -5,6 +5,7 @@ import requests_mock as requests_mock_module
from tests.modified_environ import modified_environ
from enough import settings
from enough.common import openstack
from enough.common.openstack import Stack, Heat, OpenStack
......@@ -171,3 +172,60 @@ def test_openstack_replace_volume(openstack_name):
'-f=value', '-c=Name', '--name', backup_volume).strip() == backup_volume
assert o.o.volume.list(
'-f=value', '-c=Name', '--name', openstack_name).strip() == openstack_name
@pytest.mark.skipif('SKIP_OPENSTACK_INTEGRATION_TESTS' in os.environ,
reason='skip integration test')
def test_openstack_volume_resize_ok(openstack_name):
size = 1
d = {
'name': openstack_name,
'flavor': 's1-2',
'port': '22',
'volumes': [
{
'size': str(size),
'name': openstack_name,
},
],
}
s = Stack(settings.CONFIG_DIR, d)
s.set_public_key('infrastructure_key.pub')
s.create_or_update()
o = OpenStack(settings.CONFIG_DIR)
assert size == int(o.o.volume.show(
'-c', 'size', '--format=value', openstack_name).strip())
new_size = 2
assert o.volume_resize(openstack_name, openstack_name, new_size) is True
assert new_size == int(o.o.volume.show(
'-c', 'size', '--format=value', openstack_name).strip())
assert o.volume_resize(openstack_name, openstack_name, new_size) is False
with pytest.raises(openstack.OpenStackVolumeResizeMismatch):
o.volume_resize(openstack_name, 'UNKNOWN', new_size)
with pytest.raises(openstack.OpenStackVolumeResizeShrink):
o.volume_resize(openstack_name, openstack_name, 1)
@pytest.mark.skipif('SKIP_OPENSTACK_INTEGRATION_TESTS' in os.environ,
reason='skip integration test')
def test_openstack_volume_resize_no_volume(openstack_name):
d = {
'name': openstack_name,
'flavor': 's1-2',
'port': '22',
}
s = Stack(settings.CONFIG_DIR, d)
s.set_public_key('infrastructure_key.pub')
s.create_or_update()
o = OpenStack(settings.CONFIG_DIR)
with pytest.raises(openstack.OpenStackVolumeResizeMissing):
o.volume_resize(openstack_name, openstack_name, 1)
from enough import cmd
def test_volume_resize(mocker):
# do not tamper with logging streams to avoid
# ValueError: I/O operation on closed file.
mocker.patch('cliff.app.App.configure_logging')
mocker.patch('enough.common.Enough.volume_resize')
assert cmd.main(['volume', 'resize', 'HOST', 'VOLUME']) == 0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment