Browse Source

enough: openstack helpers

keep-around/b98ffdef58152ea0074c40c0e2d3c95e6789ce6f
singuliere 2 years ago
parent
commit
3fd45c571c
No known key found for this signature in database GPG Key ID: 900857755EF189C2
  1. 99
      enough/common/openstack.py
  2. 2
      requirements-dev.in
  3. 1
      requirements-dev.txt
  4. 1
      requirements.in
  5. 2
      requirements.txt
  6. 10
      tests/enough/common/data/common/openstack/clouds.yml
  7. 63
      tests/enough/common/test_openstack.py

99
enough/common/openstack.py

@ -0,0 +1,99 @@
from bs4 import BeautifulSoup
import copy
import hashlib
import os
import requests
import sh
import yaml
class OpenStack(object):
def __init__(self, config_file):
self.config = yaml.load(open(config_file))
self.auth = self.config['clouds']['ovh']['auth']
self.horizon_session = None
def login_horizon(self):
url = 'https://horizon.cloud.ovh.net'
if self.horizon_session is None:
s = requests.Session()
r = s.get(url)
r.raise_for_status()
soup = BeautifulSoup(r.text, 'html.parser')
# print(soup.prettify())
data = {}
for input in soup.select('form[action="/auth/login/"] input'):
if input.get('name') is None or input.get('value') is None:
continue
data[input['name']] = input['value']
data['username'] = data['fake_email'] = self.auth['username']
data['password'] = data['fake_password'] = self.auth['password']
r = s.post(url + '/auth/login/', data=data, allow_redirects=False)
r.raise_for_status()
assert r.status_code == 302
assert r.headers['Location'] == url + '/'
r = s.get(r.headers['Location'], allow_redirects=False)
r.raise_for_status()
assert r.status_code == 302
assert r.headers['Location'] == url + '/project/'
self.horizon_session = s
return url, self.horizon_session
def region_list(self):
(url, s) = self.login_horizon()
r = s.get(f'{url}/project/')
r.raise_for_status()
soup = BeautifulSoup(r.text, 'html.parser')
return sorted([str(r.string) for r in soup.select('.region-name')])
def _generate_clouds(self):
clouds = {}
for region in self.region_list():
config = copy.deepcopy(self.config)
config['clouds']['ovh']['region_name'] = region
value = yaml.dump(config)
key = hashlib.md5(value.encode('utf-8')).hexdigest()
clouds[key] = value
return clouds
def generate_clouds(self, directory):
if not os.path.exists(directory):
os.makedirs(directory)
existing = os.listdir(directory)
all = self._generate_clouds()
changed = False
for new in set(all.keys()) - set(existing):
open(f'{directory}/{new}', 'w').write(all[new])
changed = True
for old in set(existing) - set(all.keys()):
old = f'{directory}/{old}'
assert os.stat(old).st_nlink == 1, f'{old} has {os.stat(old).st_nlink} links'
os.unlink(old)
changed = True
return changed
@staticmethod
def region_empty(origin):
c = sh.openstack.bake('--os-cloud=ovh', _env={
'OS_CLIENT_CONFIG_FILE': origin,
})
servers = c.server.list()
images = c.image.list('--private')
return servers.strip() == '' and images.strip() == ''
def allocate_cloud(self, directory, destination):
for f in sorted(os.listdir(directory)):
origin = f'{directory}/{f}'
os.link(origin, destination)
if os.stat(origin).st_nlink == 2 and self.region_empty(origin):
return origin
else:
os.unlink(destination)
return False

2
requirements-dev.in

@ -3,10 +3,10 @@ flake8==3.5
mock==2.0.0
pip-tools==3.5.0
pytest-cov==2.6.1
pytest-mock==1.10.4
pytest-django==3.4.8
sphinx==1.8.2
testinfra==1.14.0
tox==3.5
twine==1.13.0
dnspython==1.16.0
beautifulsoup4==4.7.1

1
requirements-dev.txt

@ -97,6 +97,7 @@ pyparsing==2.3.0 # via cliff, cmd2, packaging
pyperclip==1.7.0 # via cmd2
pytest-cov==2.6.1
pytest-django==3.4.8
pytest-mock==1.10.4
pytest==4.4.0
python-dateutil==2.7.5 # via arrow
python-gilt==1.2.1 # via molecule

1
requirements.in

@ -1,4 +1,5 @@
ansible==2.7.5
beautifulsoup4==4.7.1
cliff>=2.14
django==2.2
djangorestframework==3.9.2

2
requirements.txt

@ -14,6 +14,7 @@ asn1crypto==0.23.0 # via cryptography
atomicwrites==1.3.0 # via pytest
attrs==19.1.0 # via pytest
bcrypt==3.1.3 # via paramiko
beautifulsoup4==4.7.1
binaryornot==0.4.4 # via cookiecutter
cached-property==1.5.1 # via docker-compose
cerberus==1.2 # via molecule
@ -93,6 +94,7 @@ requestsexceptions==1.3.0 # via openstacksdk, os-client-config
sh==1.12.14
shade==1.30.0
six==1.11.0 # via ansible-lint, bcrypt, click-completion, cliff, cmd2, cryptography, docker, docker-compose, docker-pycreds, dockerpty, fasteners, keystoneauth1, molecule, munch, openstacksdk, pynacl, pytest, python-dateutil, stevedore, testinfra, websocket-client
soupsieve==1.9.1 # via beautifulsoup4
sqlparse==0.3.0 # via django
stevedore==1.26.0 # via cliff, keystoneauth1
tabulate==0.8.2 # via molecule

10
tests/enough/common/data/common/openstack/clouds.yml

@ -0,0 +1,10 @@
---
# https://docs.openstack.org/python-openstackclient/latest/configuration/index.html#configuration-files
clouds:
ovh:
auth:
auth_url: "https://auth.cloud.ovh.net/v2.0/" # OS_AUTH_URL
project_name: "12124722801405" # OS_TENANT_NAME
username: "6WmkYtVQR" # OS_USERNAME
password: "vq9VCPp4ZEPH5ZFmXjty3XEcF2E" # OS_PASSWORD
region_name: "DE1" # OS_REGION_NAME

63
tests/enough/common/test_openstack.py

@ -0,0 +1,63 @@
import os
import pytest
from enough.common.openstack import OpenStack
@pytest.mark.skipif('SKIP_OPENSTACK_INTEGRATION_TESTS' in os.environ,
reason='skip integration test')
def test_region_list():
o = OpenStack('inventories/common/group_vars/all/clouds.yml')
assert o.config['clouds']['ovh']['region_name'] in o.region_list()
@pytest.mark.skipif('SKIP_OPENSTACK_INTEGRATION_TESTS' in os.environ,
reason='skip integration test')
def test_region_empty(openstack_client):
clouds_file = 'inventories/common/group_vars/all/clouds.yml'
if OpenStack.region_empty(clouds_file):
openstack_client.image.create(
'--property=enough=fixture', '--file=/dev/null', 'remove-me')
assert not OpenStack.region_empty(clouds_file)
def test_generate_clouds(tmpdir, mocker):
o = OpenStack('tests/enough/common/data/common/openstack/clouds.yml')
directory = f'{tmpdir}/hosting'
mocker.patch.object(o, 'region_list', return_value=['REGION1', 'REGION2'])
assert o.generate_clouds(directory)
assert (sorted(os.listdir(directory)) ==
['11012f8ce408470f65eacf6daf0c132b', '742fb1a63c9a3b034dfea05e5c671602'])
# REGION2 is removed, REGION4 is added
mocker.patch.object(o, 'region_list', return_value=['REGION1', 'REGION4'])
assert o.generate_clouds(directory)
assert (sorted(os.listdir(directory)) ==
['11012f8ce408470f65eacf6daf0c132b', 'e519b99d96c0a309ec29d1a9937f2009'])
# no change
assert o.generate_clouds(directory) is False
os.link(f'{directory}/11012f8ce408470f65eacf6daf0c132b', f'{tmpdir}/in-use')
with pytest.raises(AssertionError) as e:
mocker.patch.object(o, 'region_list', return_value=['REGION4'])
o.generate_clouds(directory)
assert 'has 2 links' in str(e)
def test_allocate_cloud(tmpdir, mocker):
o = OpenStack('tests/enough/common/data/common/openstack/clouds.yml')
directory = f'{tmpdir}/hosting'
mocker.patch.object(o, 'region_list', return_value=['REGION1', 'REGION2'])
mocker.patch.object(o, 'region_empty', return_value=True)
o.generate_clouds(directory)
assert (o.allocate_cloud(directory, f'{tmpdir}/one') ==
f'{directory}/11012f8ce408470f65eacf6daf0c132b')
assert (o.allocate_cloud(directory, f'{tmpdir}/two') ==
f'{directory}/742fb1a63c9a3b034dfea05e5c671602')
assert o.allocate_cloud(directory, f'{tmpdir}/three') is False
Loading…
Cancel
Save