Browse Source

backup: implement the backup download command

keep-around/8b633916603d5b5410839869888855f5f5a625b1
Loïc Dachary 10 months ago
parent
commit
8b63391660
Signed by: dachary GPG Key ID: 992D23B392F9E4F2
  1. 1
      AUTHORS
  2. 1
      Pipfile
  3. 66
      Pipfile.lock
  4. 5
      docs/release-notes.rst
  5. 24
      docs/services/backup.rst
  6. 24
      enough/cli/backup.py
  7. 14
      enough/common/__init__.py
  8. 131
      enough/common/openstack.py
  9. 1
      enough/common/options.py
  10. 3
      requirements.txt
  11. 1
      setup.cfg
  12. 1
      tests/enough/common/test_common_options.py
  13. 67
      tests/enough/common/test_openstack.py
  14. 11
      tests/enough/test_backup.py
  15. 2
      tests/tox.dockerfile

1
AUTHORS

@ -1,4 +1,3 @@
nesousx <nesousx@nesomail.com>
Ayush Dwivedi <itsayushdwivedi@gmail.com>
Bad Bulma <bulma@badaas.top>
Debian <debian@ansible.localdomain>

1
Pipfile

@ -39,3 +39,4 @@ python-heatclient = "*"
sh = "==1.12.14"
shade = "==1.30.0"
libvirt-python = "*"
python-glanceclient = "*"

66
Pipfile.lock

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "8cc6418cf556c6d7df2d2f67166c0158565dc32c57d146569e631b73306e0e10"
"sha256": "baad3ca467720806561186050f670ed25af445edc137f5449bec573280fa3e88"
},
"pipfile-spec": 6,
"requires": {},
@ -355,8 +355,12 @@
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
@ -365,24 +369,39 @@
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
@ -595,6 +614,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.4.0"
},
"pyopenssl": {
"hashes": [
"sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51",
"sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==20.0.1"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
@ -631,6 +658,14 @@
],
"version": "==0.15.0"
},
"python-glanceclient": {
"hashes": [
"sha256:8dfb31dad2d08885e2354bba6cec29bf271f65feffb925658b312d5af0fc2af5",
"sha256:b9505dfbb207daea06e7f03c340292b87bd32d21a23bf88b8ed2eef36fd3d60f"
],
"index": "pypi",
"version": "==3.3.0"
},
"python-heatclient": {
"hashes": [
"sha256:5ec937777d13f628af3d1694fa275ada7657e1c8175356d7b24ab4be65f6c852",
@ -846,6 +881,12 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.2"
},
"warlock": {
"hashes": [
"sha256:a093c4d04b42b7907f69086e476a766b7639dca50d95edc83aef6aeab9db2090"
],
"version": "==1.3.3"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
@ -1048,8 +1089,12 @@
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
@ -1058,24 +1103,39 @@
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"

5
docs/release-notes.rst

@ -1,6 +1,11 @@
Release Notes
=============
2.1.21
------
* Add `backup download` to download the latest backup in `~/.enough/example.com/backups`.
2.1.18
------

24
docs/services/backup.rst

@ -3,7 +3,12 @@ Backups
Persistent data is placed in :ref:`encrypted volumes
<attached_volumes>` otherwise it may be deleted at any moment, when
the host fails. A daily backup of all volumes is done on the host
the host fails.
Performing backups
------------------
A daily backup of all volumes is done on the host
designated to host backups (for instance `bind-host`) when the service
is created as follows:
@ -31,3 +36,20 @@ backup.
The volumes are replicated three times and their data cannot be lost
because of a hardware failure.
Downloading backups
-------------------
It may be useful to download the latest backup from the cloud to a
local machine, as a last resort, in case the backups are unavailable
at the same time the host is unavailable.
The following command can be run manually or as a cron job:
.. code::
$ enough --domain example.com backup download \
--volumes cloud-volume --volumes wekan-volume \
--hosts cloud-host --hosts wekan-host
The backups are stored in the `$HOME/.enough/backups` directory.

24
enough/cli/backup.py

@ -1,3 +1,4 @@
import logging
from cliff.command import Command
from enough import settings
@ -57,3 +58,26 @@ class CloneVolume(Command):
args.update(vars(parsed_args))
e = Enough(settings.CONFIG_DIR, settings.SHARE_DIR, **args)
e.cli_clone_volume()
class Download(Command):
"Download backups."
log = logging.getLogger(__name__)
def get_parser(self, prog_name):
parser = super().get_parser(prog_name)
options.set_options(parser)
parser.openstack_group.add_argument(
'--volumes', action='append',
help='Download the latest snapshot of this volume (can be repeated)')
parser.openstack_group.add_argument(
'--hosts', action='append',
help='Download the latest backup image of this host (can be repeated)')
return 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.backup_download()

14
enough/common/__init__.py

@ -178,12 +178,6 @@ class Enough(object):
self._clone_volume_from_snapshot_cleanup(clone, from_volume)
clone.openstack.o.volume.set('--name', snapshot, to_volume)
def create_volume_from_snapshot(self, snapshot, volume):
if not self.openstack.o.volume.list(
'-c', 'Name', '--format=value',
'--name', volume).strip() == volume:
self.openstack.o.volume.create('--snapshot', snapshot, volume)
def _clone_volume_from_snapshot_body(self, clone, snapshot):
#
# in the current region
@ -191,7 +185,7 @@ class Enough(object):
#
volume_name = self.volume_from_snapshot(snapshot)
from_volume = f'copy-from-{volume_name}'
self.create_volume_from_snapshot(snapshot, from_volume)
self.openstack.create_volume_from_snapshot(snapshot, from_volume)
size = self.openstack.o.volume.show(
'-c', 'size', '--format=value', from_volume).strip()
@ -257,7 +251,7 @@ class Enough(object):
def restore_local(self, snapshot):
host = self.host_from_snapshot(snapshot)
volume = snapshot
self.create_volume_from_snapshot(snapshot, volume)
self.openstack.create_volume_from_snapshot(snapshot, volume)
self.openstack.replace_volume(host, volume, delete_volume=False)
return self
@ -303,6 +297,10 @@ class Enough(object):
info.append(f'\t{service}\t{" ".join(service_info)}')
return info
def backup_download(self):
if self.args.get('driver') == 'openstack':
self.openstack.backup_download(self.args['volumes'], self.args['hosts'])
def backup_create(self):
if self.args.get('driver') == 'openstack':
self.openstack.backup_create(self.args['volumes'])

131
enough/common/openstack.py

@ -311,6 +311,14 @@ class OpenStackVolumeResizeShrink(Exception):
pass
class OpenStackImageCreate(Exception):
pass
class OpenStackVolumeCreate(Exception):
pass
class OpenStack(OpenStackBase):
def __init__(self, config_dir, **kwargs):
@ -471,6 +479,87 @@ class OpenStack(OpenStackBase):
self._backup_available(volumes, date)
return count
def backup_latests(self, backups, names):
name2backup = {}
for backup in sorted(backups):
name = self.backup_extract_name(backup)
if name in names:
name2backup[name] = backup
return name2backup
def backup_download_volumes(self, volumes):
backups = self.backup_latests(self.snapshot_list(), volumes)
for volume, backup in backups.items():
self.create_volume_from_snapshot(backup, backup)
self.image_create_from_volume(backup)
self.backup_download_image(volume, backup)
self.o.image.delete(backup)
self.o.volume.delete(backup)
def backup_download_images(self, images):
for image, backup in self.backup_latests(self.image_list(), images).items():
self.backup_download_image(image, backup)
def backup_pathname(self, image):
d = f'{self.config_dir}/backups'
if not os.path.exists(d):
os.makedirs(d)
return f'{d}/{image}'
def glance(self):
config = self.config['clouds'][self.args.get('cloud', 'production')]
auth = config['auth']
if 'user_domain_name' in auth:
# OVH
e = {
'OS_AUTH_URL': auth['auth_url'],
'OS_PROJECT_ID': auth.get('project_id'),
'OS_PROJECT_NAME': auth.get('project_name'),
'OS_USER_DOMAIN_NAME': auth.get('user_domain_name'),
'OS_USER_DOMAIN_ID': 'default',
'OS_USERNAME': auth.get('username'),
'OS_PASSWORD': auth['password'],
'OS_REGION_NAME': config['region_name'],
'OS_IDENTITY_API_VERSION': str(config.get('identity_api_version')),
'OS_INTERFACE': 'public',
}
else:
# Fuga
e = {
'OS_AUTH_URL': auth['auth_url'],
'OS_PROJECT_ID': auth.get('project_id'),
'OS_PROJECT_DOMAIN_ID': auth.get('project_domain_id'),
'OS_USER_DOMAIN_ID': auth.get('user_domain_id'),
'OS_USER_ID': auth.get('user_id'),
'OS_USERNAME': auth.get('user_id'),
'OS_PASSWORD': auth['password'],
'OS_REGION_NAME': config['region_name'],
'OS_INTERFACE': config.get('interface'),
'OS_IDENTITY_API_VERSION': str(config.get('identity_api_version')),
}
return sh.glance.bake(
_tee=True,
_tty_out=False,
_out=lambda x: log.info(x.strip()),
_err=lambda x: log.info(x.strip()),
_env=e)
def backup_download_image(self, image, backup):
pathname = self.backup_pathname(image)
#
# this can be replaced once openstackclient is able to download images
# without allocating a memory amount that is equal to the size of the
# image
#
self.glance()('image-download', '--file', pathname, self.image_id(backup))
return True
def backup_download(self, volumes, hosts):
if volumes:
self.backup_download_volumes(volumes)
if hosts:
self.backup_download_images(hosts)
def _backup_map(self):
return dict(self._backup_list())
@ -513,6 +602,12 @@ class OpenStack(OpenStackBase):
else:
return None
def create_volume_from_snapshot(self, snapshot, volume):
if not self.o.volume.list(
'-c', 'Name', '--format=value',
'--name', volume).strip() == volume:
self.o.volume.create('--snapshot', snapshot, volume)
def volume_prune(self, days):
r = self.o.volume.snapshot.list(
'--format=json', '--long', '-c', 'Volume', '--limit', '5000')
@ -567,6 +662,15 @@ class OpenStack(OpenStackBase):
self.o.server.add.volume(host, volume_id)
self.o.server.start(host)
def volume_status(self, volume):
return self.o.volume.show('--format=value', '-c', 'status', volume).strip()
@retry(OpenStackVolumeCreate, tries=5)
def volume_wait_for_available(self, volume):
status = self.volume_status(volume)
if status != 'available':
raise OpenStackVolumeCreate(f'{volume} status is {status}')
def volume_resize(self, host, volume, size):
attached = self.o.server.show(
'--format=yaml', '-c', 'volumes_attached', host).stdout.strip()
@ -595,12 +699,39 @@ class OpenStack(OpenStackBase):
self.o.server.start(host)
return True
def snapshot_list(self):
return [
i.strip() for i in self.o.volume.snapshot.list(
'--format=value', '-c', 'Name', _iter=True)
]
def image_list(self):
return [
i.strip() for i in self.o.image.list(
'--private', '--format=value', '-c', 'Name', _iter=True)
]
def image_status(self, image):
return self.o.image.show('--format=value', '-c', 'status', image).strip()
def image_id(self, image):
return self.o.image.show('--format=value', '-c', 'id', image).strip()
def image_create_from_volume(self, volume):
if volume in self.image_list():
return False
# open('/dev/tty') can be removed at any point, it is a workaround
self.o.image.create('--shared', '--volume', volume, volume,
_in=open('/dev/tty'))
self.image_wait_for_active(volume)
return True
@retry(OpenStackImageCreate, tries=12)
def image_wait_for_active(self, image):
status = self.image_status(image)
if status != 'active':
raise OpenStackImageCreate(f'{image} status is {status}')
def image_backup_upload(self, name, pathname):
date = self.backup_date()
image_name = f'{date}-{name}'

1
enough/common/options.py

@ -9,4 +9,5 @@ def set_options(parser):
'--cloud',
default='production',
help='Name of the cloud in which resources are provisionned')
parser.openstack_group = o
return parser

3
requirements.txt

@ -67,11 +67,13 @@ pbr==5.5.1 ; python_version >= '2.6'
prettytable==0.7.2
pycparser==2.20 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
pynacl==1.4.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
pyopenssl==20.0.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pyparsing==2.4.7 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
pyperclip==1.8.1
pyrsistent==0.17.3 ; python_version >= '3.5'
python-cinderclient==7.2.0 ; python_version >= '3.6'
python-dotenv==0.15.0
python-glanceclient==3.3.0
python-heatclient==2.3.0
python-keystoneclient==4.2.0 ; python_version >= '3.6'
python-novaclient==17.2.1 ; python_version >= '3.6'
@ -93,6 +95,7 @@ sqlparse==0.4.1 ; python_version >= '3.5'
stevedore==3.3.0 ; python_version >= '3.6'
texttable==1.6.3
urllib3==1.26.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
warlock==1.3.3
wcwidth==0.2.5
websocket-client==0.57.0
wrapt==1.12.1

1
setup.cfg

@ -86,6 +86,7 @@ enough.cli =
volume_resize = enough.cli.volume:Resize
backup_restore = enough.cli.backup:Restore
backup_clone_volume = enough.cli.backup:CloneVolume
backup_download = enough.cli.backup:Download
enough.internal.cli =
build_image = enough.internal.cli.docker:Build

1
tests/enough/common/test_common_options.py

@ -6,6 +6,7 @@ from enough.common import options
def test_set_options():
parser = argparse.ArgumentParser()
assert options.set_options(parser) == parser
assert hasattr(parser, 'openstack_group')
args = parser.parse_args([])
driver = 'libvirt'
args = parser.parse_args(['--driver', driver])

67
tests/enough/common/test_openstack.py

@ -96,6 +96,12 @@ def test_create_test_subdomain_with_bind(tmpdir, mocker, requests_mock):
#
# OpenStack
#
@pytest.mark.openstack_integration
def test_glance(dot_openstack):
o = OpenStack(dot_openstack.config_dir)
assert len(o.glance('image-list')) > 0
def test_server_ip_in_network_ok(mocker):
ip = '1.2.3.4'
mocker.patch('enough.common.openstack.OpenStack.server_port_list',
@ -304,3 +310,64 @@ def test_image_backup_prune(tmpdir, mocker, openstack_name, dot_openstack):
o.image_backup_prune([openstack_name], days)
assert o.image_list() == [o.backup_name_create(openstack_name)]
def test_backup_latests():
o = OpenStack(settings.CONFIG_DIR)
name = 'name1'
latest = f'2021-03-02-{name}'
assert o.backup_latests(
['ignorethat', latest, f'2020-03-02-{name}', '2021-03-02-name2'],
[name]
) == {name: latest}
@pytest.mark.openstack_integration
def test_backup_download_images(tmpdir, openstack_name, dot_openstack):
original_pathname = f'{tmpdir}/test.qcow2'
sh.qemu_img('create', '-f', 'qcow2', original_pathname, '1M')
original_md5 = sh.md5sum(original_pathname).split(' ')[0]
o = OpenStack(dot_openstack.config_dir)
assert o.image_backup_upload(openstack_name, original_pathname) is True
o.backup_download(volumes=[], hosts=[openstack_name])
backup_pathname = o.backup_pathname(openstack_name)
backup_md5 = sh.md5sum(backup_pathname).split(' ')[0]
assert backup_md5 == original_md5
@pytest.mark.openstack_integration
def test_backup_download_volumes(tmpdir, openstack_name, dot_openstack):
o = OpenStack(dot_openstack.config_dir)
#
# create a small volume
#
o.o.volume.create('--size=1', openstack_name)
#
# create a backup
#
o.backup_create([])
#
# download the backup
#
o.backup_download(volumes=[openstack_name], hosts=[])
backup_pathname = o.backup_pathname(openstack_name)
assert os.path.exists(backup_pathname)
@pytest.mark.openstack_integration
def test_image_create_from_volume(openstack_name, dot_openstack):
o = OpenStack(dot_openstack.config_dir)
o.o.volume.create('--size=1', openstack_name)
o.volume_wait_for_available(openstack_name)
assert o.image_list() == []
o.image_create_from_volume(openstack_name)
assert o.image_status(openstack_name) == 'active'
li = [
i.strip() for i in o.o.image.list(
'--shared', '--format=value', '-c', 'Name', _iter=True)
]
assert li == [openstack_name]
o.o.image.delete(openstack_name)

11
tests/enough/test_backup.py

@ -15,3 +15,14 @@ def test_backup_clone_volume(mocker, tmp_config_dir):
mocker.patch('cliff.app.App.configure_logging')
mocker.patch('enough.common.Enough.cli_clone_volume')
assert cmd.main(['--debug', 'backup', 'clone', 'volume', 'name']) == 0
def test_enough_download(capsys, mocker, tmp_config_dir):
# 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.backup_download',
side_effect=lambda *args, **kwargs: print('DOWNLOAD'))
assert cmd.main(['backup', 'download']) == 0
out, err = capsys.readouterr()
assert 'DOWNLOAD' in out

2
tests/tox.dockerfile

@ -1,4 +1,4 @@
RUN pip install python-openstackclient python-heatclient # this is not necessary to run tests but to cleanup leftovers when tests fail
RUN pip install python-openstackclient python-heatclient python-glanceclient # this is not necessary to run tests but to cleanup leftovers when tests fail
RUN pip install tox
# BEGIN dependencies playbooks/infrastructure/test-pipelining.yml
RUN apt-get install -y python

Loading…
Cancel
Save