Browse Source

api: allow authentication via GitLab

keep-around/6db9ad37d2baa3f1a96f483f8daccd2ebf098c3c
singuliere 2 years ago
parent
commit
6db9ad37d2
No known key found for this signature in database GPG Key ID: 900857755EF189C2
  1. 2
      enough/api/data/enough.service
  2. 2
      enough/auth/__init__.py
  3. 7
      enough/auth/apps.py
  4. 0
      enough/auth/management/__init__.py
  5. 0
      enough/auth/management/commands/__init__.py
  6. 40
      enough/auth/management/commands/set_auth_provider.py
  7. 2
      enough/cmd.py
  8. 1
      enough/common/docker.py
  9. 3
      enough/internal/data/docker-compose.yml
  10. 2
      enough/internal/data/enough-source.dockerfile
  11. 48
      enough/settings.py
  12. 96
      enough/static/css/main.css
  13. 10
      enough/templates/allauth/socialaccount/authentication_error.html
  14. 29
      enough/templates/allauth/socialaccount/signup.html
  15. 14
      enough/templates/bases/bootstrap-jquery.html
  16. 29
      enough/templates/bases/bootstrap-member.html
  17. 26
      enough/templates/bases/bootstrap-visitor.html
  18. 46
      enough/templates/bases/bootstrap.html
  19. 26
      enough/templates/member/member-index.html
  20. 14
      enough/templates/visitor/landing-index.html
  21. 13
      enough/urls.py
  22. 10
      enough/views.py
  23. 1
      molecule/api/tests/gitlab_utils.py
  24. 71
      molecule/api/tests/test_api.py
  25. 5
      molecule/packages/roles/enough-pip/tasks/enough-pip.yml
  26. 1
      requirements-dev.in
  27. 10
      requirements-dev.txt
  28. 2
      requirements.in
  29. 8
      requirements.txt

2
enough/api/data/enough.service

@ -4,7 +4,7 @@ Requires=network-online.target
After=network-online.target
[Service]
ExecStart=env PATH=/opt/venv/bin:${PATH} enough manage runserver 0.0.0.0:8000
ExecStart=env PATH=/opt/venv/bin:${PATH} REQUESTS_CA_BUNDLE=/etc/ssl/certs enough --domain {{ this.app.options.domain }} manage runserver 0.0.0.0:8000
[Install]
WantedBy=multi-user.target

2
enough/auth/__init__.py

@ -0,0 +1,2 @@
# See https://docs.djangoproject.com/en/1.7/ref/applications/#for-application-authors
default_app_config = 'enough.auth.apps.EnoughAuthAppConfig'

7
enough/auth/apps.py

@ -0,0 +1,7 @@
from django.apps import AppConfig
class EnoughAuthAppConfig(AppConfig):
label = "enough_auth"
name = "enough.auth"
verbose_name = "Enough Auth"

0
enough/auth/management/__init__.py

0
enough/auth/management/commands/__init__.py

40
enough/auth/management/commands/set_auth_provider.py

@ -0,0 +1,40 @@
from allauth.socialaccount.models import SocialApp
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Update or insert a specific allauth provider entry like Google or Facebook'
def add_arguments(self, parser):
parser.add_argument('provider', type=str, help="Provider ID like 'google'")
parser.add_argument('--name', default=None,
type=str, help="Display name like 'Google' (optional)")
parser.add_argument('client_id',
type=str, help="Client ID from provider (==Facebook App ID)")
parser.add_argument('client_secret',
type=str, help="Secret from provider (==Facebook App Secret)")
def handle(self, *args, **options):
provider = options['provider']
name = options.get('name') or provider.title()
client_id = options['client_id']
client_secret = options['client_secret']
# Delete the specific provider if it exists.
# Note that QuerySet.update_or_create is an alternative.
SocialApp.objects.filter(provider=provider).delete()
a = SocialApp(provider=provider, name=name, secret=client_secret,
client_id=client_id, key='')
a.save()
# Now associate this provider with all site instances.
sites = [i for i in Site.objects.all()]
a.sites.add(*sites)
self.stdout.write(self.style.SUCCESS(
"Provider '{}' -> client ID '{}'".format(a.name, a.client_id)
))
self.stdout.write(self.style.SUCCESS(
"Associated with site(s): {}".format(', '.join(s.name for s in sites))
))

2
enough/cmd.py

@ -18,7 +18,7 @@ class EnoughApp(App):
def build_option_parser(self, description, version, argparse_kwargs=None):
parser = super().build_option_parser(description, version, argparse_kwargs)
parser.add_argument('--domain', help='Enough domain name')
parser.add_argument('--domain', default='enough.community', help='Enough domain name')
return parser

1
enough/common/docker.py

@ -8,7 +8,6 @@ import sh
import tempfile
from enough.version import __version__
from enough import configuration
from enough.common.retry import retry, RetryException
log = logging.getLogger(__name__)

3
enough/internal/data/docker-compose.yml

@ -19,7 +19,8 @@ services:
- /run/lock
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
- {{ this.confdir }}:/root/.enough/default
- /root/.enough/{{ this.domain }}:/root/.enough/{{ this.domain }}
- /usr/local/share/ca-certificates/infrastructure:/usr/local/share/ca-certificates/infrastructure:ro
security_opt:
- seccomp=unconfined
cap_add:

2
enough/internal/data/enough-source.dockerfile

@ -7,7 +7,7 @@ RUN pip3 install -r /tmp/requirements.txt
COPY dist/* .
RUN pip3 install *.tar.gz
RUN python -m enough.internal.cmd install --service > /etc/systemd/system/enough.service && systemctl enable enough
RUN python -m enough.internal.cmd install api/data/enough.service > /etc/systemd/system/enough.service && systemctl enable enough
CMD [ "help" ]
ENTRYPOINT [ "python", "-m", "enough.internal.cmd" ]

48
enough/settings.py

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
"""
import os
from os.path import dirname, join
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
ENOUGH_DOMAIN = os.environ.get('ENOUGH_DOMAIN', 'enough.community')
@ -38,9 +39,18 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'rest_framework',
'rest_framework.authtoken',
'enough.api',
'bootstrap3',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.gitlab', # enabled by configure
'enough.auth',
]
MIDDLEWARE = [
@ -55,15 +65,25 @@ MIDDLEWARE = [
ROOT_URLCONF = 'enough.urls'
AUTHENTICATION_BACKENDS = (
"allauth.account.auth_backends.AuthenticationBackend",
)
MODULE_DIR = dirname(__file__)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [
join(MODULE_DIR, 'templates', 'allauth'),
join(MODULE_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.template.context_processors.static',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
@ -123,7 +143,33 @@ USE_TZ = True
STATIC_URL = '/static/'
STATICFILES_DIRS = (
join(MODULE_DIR, "static"),
)
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
ENOUGH_GITLAB_URL = f'https://lab.{ENOUGH_DOMAIN}'
SITE_ID = 1
ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'https'
LOGIN_REDIRECT_URL = '/member/'
ACCOUNT_AUTHENTICATION_METHOD = 'username'
ACCOUNT_LOGOUT_ON_GET = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_USERNAME_MIN_LENGTH = 1
ACCOUNT_EMAIL_VERIFICATION = 'none' # testing...
# ACCOUNT_USER_MODEL_USERNAME_FIELD = None
SOCIALACCOUNT_AUTO_SIGNUP = False # require social accounts to use the signup form ... I think
# For custom sign-up form:
# http://stackoverflow.com/questions/12303478/how-to-customize-user-profile-when-using-django-allauth
SOCIALACCOUNT_PROVIDERS = {
'gitlab': {
'GITLAB_URL': ENOUGH_GITLAB_URL,
'SCOPE': ['read_user', 'api'],
},
}

96
enough/static/css/main.css

@ -0,0 +1,96 @@
.big-box {
border: 1px solid #eee;
-webkit-box-shadow: 0 3px 3px rgba(0,0,0,.5);
box-shadow: 0 3px 3px rgba(0,0,0,.5);
padding: 20px;
margin-top: 60px;
margin-bottom: 60px;
/*
-webkit-box-shadow:0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;
-moz-box-shadow:0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;
box-shadow:0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;
*/
}
.socialaccount_providers li a.socialaccount_provider {
rem-border: 1px solid #ddd;
border-radius: 10px;
rem-box-shadow: 2px 2px 8px rgba(0,0,0,.7);
-padding: 10px;
margin-bottom: 6px;
display: block;
width: 100%;
overflow: hidden;
rem-font-size: 1.2em;
}
.socialaccount_providers li a.socialaccount_provider:hover {
text-decoration: none;
box-shadow: 1px 1px 2px rgba(0,0,0,.7);
}
.socialaccount_providers li a.socialaccount_provider.facebook {
background: #4B67A3; /* technically gradient down to #3A579A */
color: #fff;
}
.socialaccount_providers li a.socialaccount_provider.facebook:before {
font-family: FontAwesome;
font-size: 1.9em;
content: "\f082";
display: inline-block;
padding-left: 8px;
padding-right: 8px;
}
.socialaccount_providers li a.socialaccount_provider.google {
background: #DE4931;
color: #fff;
}
.socialaccount_providers li a.socialaccount_provider.google:before {
font-family: FontAwesome;
font-size: 1.9em;
content: "\f1a0";
display: inline-block;
padding-left: 8px;
padding-right: 8px;
}
/* from http://konigi.com/tools/css-sticky-notes */
.sticky {
-webkit-box-shadow: #DDD 0px 1px 2px;
position: relative;
background-color: #F4F39E;
border-color: #DEE184;
text-align: center;
margin: 2.5em 0px;
padding: 1.5em 1em;
-webkit-box-shadow: 0px 1px 3px rgba(0,0,0,0.25);
-moz-box-shadow: 0px 1px 3px rgba(0,0,0,0.25);
box-shadow: 0px 1px 3px rgba(0,0,0,0.25);
font-family: Chalkboard, 'Comic Sans';
}
.sticky.taped2:after {
display: block;
content: "";
position: absolute;
width: 110px;
height: 30px;
top: -21px;
left: 30%;
border: 1px solid #fff;
background: rgba(254, 254, 254, .6);
-webkit-box-shadow: 0px 0 3px rgba(0,0,0,0.1);
-moz-box-shadow: 0px 0 3px rgba(0,0,0,0.1);
box-shadow: 0px 0 3px rgba(0,0,0,0.1);
}
/* borrowed from http://designshack.net/articles/css/4-fun-css-image-effects-you-can-copy-and-paste/ */
.picture-frame img {
border: 10px solid #fff;
-webkit-box-shadow: 3px 0px 0px #777;
-moz-box-shadow: 3px 0px 0px #777;
box-shadow: 0px 0px 5px rgba(0,0,0,0.35);
}

10
enough/templates/allauth/socialaccount/authentication_error.html

@ -0,0 +1,10 @@
{% extends "bases/bootstrap-auth.html" %}
{% block head_title %}{% trans "Social Network Login Failure" %}{% endblock %}
{% block content %}
<h1>{% trans "Social Network Login Failure" %}</h1>
<p>{% trans "An error occurred while attempting to login via your social network account." %}</p>
<p>{{ auth_error }}</p>
{% endblock %}

29
enough/templates/allauth/socialaccount/signup.html

@ -0,0 +1,29 @@
{% extends "bases/bootstrap-member.html" %}
{% load bootstrap3 %}
{% block head_title %}Signup{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<p>As a final step, please complete the following form:</p>
<form class="signup" id="signup_form" method="post" action="">
{% csrf_token %}
{% bootstrap_form form %}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<div class="form-actions">
<button class="btn btn-primary" type="submit">Sign In</button>
</div>
</form>
</div>
</div>
{% endblock %}

14
enough/templates/bases/bootstrap-jquery.html

@ -0,0 +1,14 @@
{% extends "bases/bootstrap.html" %}
{% load staticfiles %}
{% block tail_js %}
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<!-- <script>window.jQuery || document.write('<script src="js/jquery-1.10.1.min.js"><\/script>')</script> -->
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
{% block app_js %}
{% endblock %}
{% endblock %}

29
enough/templates/bases/bootstrap-member.html

@ -0,0 +1,29 @@
{% extends "bases/bootstrap-jquery.html" %}
{% load staticfiles %}
{% block nav %}
<!-- Fixed navbar -->
<div class="navbar navbar-default navbar-static-top navbar-shadow" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{% url 'user_home' %}">Enough Community Member</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="{% url 'landing_index' %}">Home</a></li>
<li><a href="{% url 'account_logout' %}"></i> Sign Out</a></li>
<li>{% if user.profile.avatar_url %}<img alt="" style="width:50px; height:50px" src="{{user.profile.avatar_url}}">{% endif %}</li>
</ul>
</div>
</div>
</div>
{% endblock %}

26
enough/templates/bases/bootstrap-visitor.html

@ -0,0 +1,26 @@
{% extends "bases/bootstrap-jquery.html" %}
{% load staticfiles %}
{% block nav %}
<!-- Fixed navbar -->
<div class="navbar navbar-default navbar-static-top navbar-shadow" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Enough API</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="{% url 'gitlab_login' %}?process=login"><strong>Sign In</strong></a></li>
</ul>
</div>
</div>
</div>
{% endblock %}

46
enough/templates/bases/bootstrap.html

@ -0,0 +1,46 @@
{% load staticfiles %}
<!DOCTYPE html>
<html lang="en" {% block htmlattr %}{% endblock %}>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}allauth demo{% endblock %}</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
{% block head_css %}
<link rel="stylesheet" media="screen" href='{% static "css/main.css" %}'>
{% endblock %}
</head>
<body class='{% block pageclass %}{% endblock %}' {% block bodyattr %}{% endblock %}>
{% block nav %}{% endblock %}
{% block content %}{% endblock %}
{% block footer %}
<div class=container>
<hr>
<footer>
</footer>
</div>
{% endblock %}
{% block tail_js %}
{% endblock %}
</body>
</html>

26
enough/templates/member/member-index.html

@ -0,0 +1,26 @@
{% extends "bases/bootstrap-member.html" %}
{% load bootstrap3 %}
{% block head_title %}Welcome{% endblock %}
{% block content %}
<div class="container">
<div class="row">
{% bootstrap_messages %}
<div class="col-sm-6 col-md-4 col-lg-4">
<h3>Profile</h3>
<ul class="list-unstyled">
<li>First name: <strong>{{user.first_name}}</strong></li>
<li>Last name: <strong>{{user.last_name}}</strong></li>
<li>Display name: <strong>{{user.display_name}}</strong></li>
<li>Email: <strong>{{user.email}}</strong></li>
<li>{% if request.user.profile.account_verified %}Verified{% else %}Unverified{% endif %}</li>
</ul>
</div>
</div>
</div>
{% endblock %}

14
enough/templates/visitor/landing-index.html

@ -0,0 +1,14 @@
{% extends "bases/bootstrap-visitor.html" %}
{% load staticfiles %}
{% block content %}
<div class="container">
<div class="jumbotron text-center">
<h1>Enough API</h1>
</div>
</div>
{% endblock %}

13
enough/urls.py

@ -15,9 +15,20 @@ Including another URLconf
"""
from django.urls import path
from enough.api import views
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.conf import settings
from django.views.generic import TemplateView
from .views import member_index
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
path('bind/', views.bind, name='bind'),
]
url(r'^$', TemplateView.as_view(template_name='visitor/landing-index.html'),
name='landing_index'),
url(r'^accounts/', include('allauth.urls')),
url(r'^member/$', member_index, name='user_home'),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

10
enough/views.py

@ -0,0 +1,10 @@
from django.http import HttpResponse
from django.template import loader
from django.contrib.auth.decorators import login_required
@login_required
def member_index(request):
t = loader.get_template('member/member-index.html')
c = {}
return HttpResponse(t.render(c, request), content_type='text/html')

1
molecule/api/tests/gitlab_utils.py

@ -0,0 +1 @@
../../gitlab/tests/gitlab_utils.py

71
molecule/api/tests/test_api.py

@ -2,6 +2,9 @@ import requests
import yaml
import dns.resolver
import re
import gitlab_utils
from enough.common.gitlab import GitLab
from bs4 import BeautifulSoup
testinfra_hosts = ['api-host']
@ -46,3 +49,71 @@ def test_add_host(host):
bind_ip = str(resolver.query(f'bind.{domain}.')[0])
resolver.nameservers = [bind_ip]
assert '1.2.3.4' == str(resolver.query(f'foo.{domain}.', 'a')[0])
def tests_api_sign_in(host):
domain = get_domain()
gitlab = GitLab(gitlab_utils.get_url(), 'root', gitlab_utils.get_password())
(client_id, client_secret) = gitlab.create_api_application(domain)
with host.sudo():
r = host.run(f"enough --domain {domain} manage "
f"set_auth_provider gitlab {client_id} {client_secret}")
#
# GitLab home page
#
lab = requests.Session()
lab.verify = '../../certs'
lab_url = f'https://lab.{domain}'
r = lab.get(lab_url + '/users/sign_in')
soup = BeautifulSoup(r.text, 'html.parser')
authenticity_token = soup.select(
'form[action="/users/sign_in"] input[name="authenticity_token"]')[0]['value']
r.raise_for_status()
#
# GitLab Login
#
r = lab.post(lab_url + '/users/sign_in', data={
'authenticity_token': authenticity_token,
'user[login]': 'root',
'user[password]': gitlab_utils.get_password(),
'user[remember_me]': 0,
})
r.raise_for_status()
#
# API login
#
api = requests.Session()
api.verify = '../../certs'
api.url = f'https://api.{domain}'
r = api.get(api.url + '/accounts/gitlab/login/?process=login', allow_redirects=False)
assert 'oauth/authorize' in r.headers['Location']
r.raise_for_status()
#
# GitLab OAuth confirmation page
#
r = lab.get(r.headers['Location'])
soup = BeautifulSoup(r.text, 'html.parser')
data = {
'commit': 'Authorize',
}
for input in soup.select('form[action="/oauth/authorize"]:nth-child(2) input[type="hidden"]'):
if input.get('name') is None or input.get('value') is None:
continue
data[input['name']] = input['value']
# print(soup.prettify())
r = lab.post(lab_url + '/oauth/authorize', data=data, allow_redirects=False)
r.raise_for_status()
assert 'accounts/gitlab/login/callback' in r.headers['Location']
#
# API member page
#
r = api.get(r.headers['Location'])
r.raise_for_status()
assert 'Enough Community Member' in r.text

5
molecule/packages/roles/enough-pip/tasks/enough-pip.yml

@ -66,7 +66,10 @@
cd $d
curl -q https://{{ packages_vhost_fqdn }}/docker-enough/base.dockerfile > Dockerfile
curl -q https://{{ packages_vhost_fqdn }}/docker-enough/enough.dockerfile >> Dockerfile
sed -i -e "s/replace this comment/$(date +%s)/" Dockerfile
sed -i \
-e "s/replace this comment/$(date +%s)/" \
-e "s/domain enough.community/domain {{ domain }}/" \
Dockerfile
docker build --build-arg PIP3_OPTS='--extra-index-url=https://{{ packages_vhost_fqdn }}/ --trusted-host={{ packages_vhost_fqdn }}' --build-arg ENOUGH_VERSION={{ enough_version.stdout }} -t enough .
cd -
rm -fr $d

1
requirements-dev.in

@ -9,3 +9,4 @@ testinfra==1.14.0
tox==3.5
twine==1.13.0
dnspython==1.16.0
beautifulsoup4==4.7.1

10
requirements-dev.txt

@ -16,6 +16,7 @@ atomicwrites==1.2.1 # via pytest
attrs==18.2.0 # via pytest
babel==2.6.0 # via sphinx
bcrypt==3.1.5 # via paramiko
beautifulsoup4==4.7.1
binaryornot==0.4.4 # via cookiecutter
bleach==3.1.0 # via readme-renderer
cached-property==1.5.1 # via docker-compose
@ -32,6 +33,9 @@ cookiecutter==1.6.0 # via molecule
coverage==4.5
cryptography==2.4.2 # via ansible, openstacksdk, paramiko
decorator==4.3.0 # via openstacksdk
defusedxml==0.5.0 # via python3-openid
django-allauth==0.39.1
django-bootstrap3==11.0.0
django==2.2
djangorestframework==3.9.2
dnspython==1.16.0
@ -66,6 +70,7 @@ more-itertools==5.0.0 # via pytest
munch==2.3.2 # via openstacksdk
netaddr==0.7.19
netifaces==0.10.9 # via openstacksdk
oauthlib==3.0.1 # via requests-oauthlib
openstacksdk==0.22.0
os-client-config==1.31.2 # via shade
os-service-types==1.4.0 # via keystoneauth1, openstacksdk
@ -95,16 +100,19 @@ pytest-django==3.4.8
pytest==4.4.0
python-dateutil==2.7.5 # via arrow
python-gilt==1.2.1 # via molecule
python3-openid==3.1.0 # via django-allauth
pytz==2018.9 # via babel, django
pyyaml==3.12 # via ansible, ansible-lint, cliff, docker-compose, molecule, openstacksdk, python-gilt, yamllint
readme-renderer==24.0 # via twine
requests-oauthlib==1.2.0 # via django-allauth
requests-toolbelt==0.9.1 # via twine
requests==2.20.1 # via cookiecutter, docker, docker-compose, keystoneauth1, requests-toolbelt, sphinx, twine
requests==2.20.1 # via cookiecutter, django-allauth, docker, docker-compose, keystoneauth1, requests-oauthlib, requests-toolbelt, sphinx, twine
requestsexceptions==1.4.0 # via openstacksdk
sh==1.12.14
shade==1.30.0
six==1.11.0 # via ansible-lint, bcrypt, bleach, click-completion, cliff, cmd2, cryptography, docker, docker-compose, docker-pycreds, dockerpty, fasteners, keystoneauth1, mock, molecule, more-itertools, munch, openstacksdk, packaging, pip-tools, pynacl, pytest, python-dateutil, readme-renderer, sphinx, stevedore, testinfra, tox, websocket-client
snowballstemmer==1.2.1 # via sphinx
soupsieve==1.9.1 # via beautifulsoup4
sphinx==1.8.2
sphinxcontrib-websupport==1.1.0 # via sphinx
sqlparse==0.3.0 # via django

2
requirements.in

@ -2,6 +2,8 @@ ansible==2.7.5
cliff>=2.14
django==2.2
djangorestframework==3.9.2
django-allauth==0.39.1
django-bootstrap3==11.0.0
docker
docker-compose
-e git+https://github.com/fmnisme/python-icinga2api.git@9a1a3cc7968d6c72bf49e97ef387b2824e6835e9#egg=icinga2api

8
requirements.txt

@ -28,6 +28,9 @@ colorama==0.3.9 # via molecule, python-gilt
cookiecutter==1.6.0 # via molecule
cryptography==2.4.2 # via ansible, openstacksdk, paramiko
decorator==4.1.2 # via openstacksdk
defusedxml==0.5.0 # via python3-openid
django-allauth==0.39.1
django-bootstrap3==11.0.0
django==2.2
djangorestframework==3.9.2
docker-compose==1.24.0
@ -57,6 +60,7 @@ more-itertools==7.0.0 # via pytest
munch==2.2.0 # via openstacksdk
netaddr==0.7.19
netifaces==0.10.6 # via openstacksdk
oauthlib==3.0.1 # via requests-oauthlib
openstacksdk==0.22.0
os-client-config==1.28.0 # via shade
os-service-types==1.4.0 # via keystoneauth1, openstacksdk
@ -80,9 +84,11 @@ pyperclip==1.7.0 # via cmd2
pytest==4.4.0
python-dateutil==2.8.0 # via arrow
python-gilt==1.2.1 # via molecule
python3-openid==3.1.0 # via django-allauth
pytz==2018.9 # via django
pyyaml==3.12 # via ansible, ansible-lint, cliff, docker-compose, molecule, openstacksdk, os-client-config, python-gilt, yamllint
requests==2.20.1 # via cookiecutter, docker, docker-compose, keystoneauth1
requests-oauthlib==1.2.0 # via django-allauth
requests==2.20.1 # via cookiecutter, django-allauth, docker, docker-compose, keystoneauth1, requests-oauthlib
requestsexceptions==1.3.0 # via openstacksdk, os-client-config
sh==1.12.14
shade==1.30.0

Loading…
Cancel
Save