Andrew Bogott has submitted this change and it was merged. Change subject: openstack: Add proxy panel files ......................................................................
openstack: Add proxy panel files Bug: T129245 Change-Id: I785bf87a58c9361c9c92e51c2df64215665733fa --- A modules/openstack/files/liberty/horizon/proxy/__init__.py A modules/openstack/files/liberty/horizon/proxy/panel.py A modules/openstack/files/liberty/horizon/proxy/templates/proxy/_create.html A modules/openstack/files/liberty/horizon/proxy/templates/proxy/create.html A modules/openstack/files/liberty/horizon/proxy/templates/proxy/index.html A modules/openstack/files/liberty/horizon/proxy/urls.py A modules/openstack/files/liberty/horizon/proxy/views.py A modules/openstack/files/liberty/horizon/proxy_enable.py M modules/openstack/manifests/horizon/service.pp 9 files changed, 365 insertions(+), 0 deletions(-) Approvals: Andrew Bogott: Looks good to me, approved jenkins-bot: Verified diff --git a/modules/openstack/files/liberty/horizon/proxy/__init__.py b/modules/openstack/files/liberty/horizon/proxy/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/proxy/__init__.py diff --git a/modules/openstack/files/liberty/horizon/proxy/panel.py b/modules/openstack/files/liberty/horizon/proxy/panel.py new file mode 100644 index 0000000..b128a67 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/proxy/panel.py @@ -0,0 +1,20 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ +import horizon + + +class Proxy(horizon.Panel): + name = _("Web Proxies") + slug = "proxy" + policy_rules = (("dns", "get_records"),) diff --git a/modules/openstack/files/liberty/horizon/proxy/templates/proxy/_create.html b/modules/openstack/files/liberty/horizon/proxy/templates/proxy/_create.html new file mode 100644 index 0000000..cfecda6 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/proxy/templates/proxy/_create.html @@ -0,0 +1,12 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + + +{% block modal-body-right %} + <h3>{% trans "Description:" %}</h3> + <p> + {% trans "<a href='https://wikitech.wikimedia.org/wiki/Help:Proxy'>We can put some useful text explaining the form here.</a>" %} + </p> +{% endblock %} diff --git a/modules/openstack/files/liberty/horizon/proxy/templates/proxy/create.html b/modules/openstack/files/liberty/horizon/proxy/templates/proxy/create.html new file mode 100644 index 0000000..52c987d --- /dev/null +++ b/modules/openstack/files/liberty/horizon/proxy/templates/proxy/create.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create A Proxy" %}{% endblock %} + +{% block main %} + {% include 'project/proxy/_create.html' %} +{% endblock %} + diff --git a/modules/openstack/files/liberty/horizon/proxy/templates/proxy/index.html b/modules/openstack/files/liberty/horizon/proxy/templates/proxy/index.html new file mode 100644 index 0000000..788a093 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/proxy/templates/proxy/index.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Proxy" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Proxy") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} + + diff --git a/modules/openstack/files/liberty/horizon/proxy/urls.py b/modules/openstack/files/liberty/horizon/proxy/urls.py new file mode 100644 index 0000000..e39bfa8 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/proxy/urls.py @@ -0,0 +1,21 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls import url + +from wikimediaproxydashboard import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), +] diff --git a/modules/openstack/files/liberty/horizon/proxy/views.py b/modules/openstack/files/liberty/horizon/proxy/views.py new file mode 100644 index 0000000..849cf65 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/proxy/views.py @@ -0,0 +1,270 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.conf import settings +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ungettext_lazy +from django.utils.translation import ugettext_lazy as _ + +from django.forms import TextInput + +from horizon import exceptions +from horizon import forms +from horizon import tables + +from openstack_dashboard.api import base, nova + +# Designate v1 API, for normal use +import designatedashboard.api.designate as designateapi +from designateclient.v1.records import Record + +# Designate v2 API, currently only for wmflabs.org +from keystoneclient.auth.identity import generic as identity_generic +from keystoneclient import session as keystone_session +from designateclient.v2 import client as designateclientv2 + +import json +import requests +import socket +import urlparse + +LOG = logging.getLogger(__name__) + + +class CreateProxy(tables.LinkAction): + name = "create" + verbose_name = _("Create Proxy") + url = "horizon:project:proxy:create" + classes = ("ajax-modal",) + icon = "plus" + policy_rules = (("dns", "create_record"),) + + +class DeleteProxy(tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy(u"Delete Proxy", u"Delete Proxies", count) + + @staticmethod + def action_past(count): + return ungettext_lazy(u"Deleted Proxy", u"Deleted Proxies", count) + + policy_rules = (("dns", "delete_record"),) + + def delete(self, request, obj_id): + record = obj_id[:obj_id.find('.')] + domain = obj_id[obj_id.find('.') + 1:] + + if domain == 'wmflabs.org.': + auth = identity_generic.Password( + auth_url=base.url_for(request, 'identity'), + username=getattr(settings, "WMFLABSDOTORG_ADMIN_USERNAME", ''), + password=getattr(settings, "WMFLABSDOTORG_ADMIN_PASSWORD", ''), + tenant_name='wmflabsdotorg', + user_domain_id='default', + project_domain_id='default' + ) + c = designateclientv2.Client(session=keystone_session.Session(auth=auth)) + + # Delete the record from the wmflabsdotorg project. This is needed since wmflabs.org lives + # in that project and designate (quite reasonably) prevents subdomain deletion elsewhere. + zoneid = None + for zone in c.zones.list(): + if zone['name'] == 'wmflabs.org.': + zoneid = zone['id'] + break + else: + raise Exception("No zone ID") + recordsetid = None + for recordset in c.recordsets.list(zoneid): + if recordset['type'] == 'A' and recordset['name'] == record + '.' + domain: + recordsetid = recordset['id'] + break + else: + raise Exception("No recordset ID") + c.recordsets.delete(zoneid, recordsetid) + else: + c = designateapi.designateclient(request) + domainid = None + for d in c.domains.list(): + if d.name == domain: + domainid = d.id + break + else: + LOG.warn('Woops! Failed domain ID for domain ' + domain) + raise Exception("No domain ID") + recordid = None + for r in c.records.list(domainid): + if r.name == obj_id and r.type == 'A': + recordid = r.id + break + else: + LOG.warn('Woops! Failed record ID for record ' + record) + raise Exception("No record ID") + + c.records.delete(domainid, recordid) + + resp = requests.delete(base.url_for(request, 'proxy') + '/mapping/' + obj_id) + if not resp: + raise Exception("Got status " + resp.status_code) + + +def get_proxy_backends(proxy): + return ', '.join(proxy.backends) + + +class ProxyTable(tables.DataTable): + domain = tables.Column("domain", verbose_name=_("DNS Hostname"),) + backends = tables.Column(get_proxy_backends, verbose_name=_("Backends")) + + class Meta(object): + name = "proxies" + verbose_name = _("Proxies") + table_actions = (CreateProxy,) + row_actions = (DeleteProxy,) + + +class Proxy(): + def __init__(self, domain, backends): + self.id = self.domain = domain + self.backends = backends + + +class IndexView(tables.DataTableView): + table_class = ProxyTable + template_name = 'project/proxy/index.html' + page_title = _("Proxies") + + def get_data(self): + resp = None + try: + resp = requests.get(base.url_for(self.request, 'proxy') + '/mapping') + if resp.status_code == 400 and resp.text == 'No such project': + proxies = [] + elif not resp: + raise Exception("Got status " + str(resp.status_code)) + else: + proxies = [Proxy(route['domain'], route['backends']) for route in resp.json()['routes']] + except Exception: + proxies = [] + exceptions.handle(self.request, _("Unable to retrieve proxies: " + resp.text)) + return proxies + + +class CreateProxyForm(forms.SelfHandlingForm): + record = forms.CharField(max_length=255, label=_("Record")) + domain = forms.ChoiceField(widget=forms.Select(), label=_("Domain")) + backendInstance = forms.ChoiceField(widget=forms.Select(), label=_("Backend instance")) + backendPort = forms.CharField(widget=TextInput(attrs={'type': 'number'}), label=_("Backend port")) + + def __init__(self, request, *args, **kwargs): + kwargs['initial']['backendPort'] = 80 + super(CreateProxyForm, self).__init__(request, *args, **kwargs) + self.fields['backendInstance'].choices = self.populate_instances(request) + self.fields['domain'].choices = self.populate_domains(request) + + def populate_instances(self, request): + results = [(None, 'Select an instance')] + for server in nova.novaclient(request).servers.list(): + results.append((server.name, server.name)) + return results + + def populate_domains(self, request): + results = [(None, 'Select a domain'), ('wmflabs.org.', 'wmflabs.org.')] + for domain in designateapi.designateclient(request).domains.list(): + results.append((domain.name, domain.name)) + return results + + def clean(self): + cleaned_data = super(CreateProxyForm, self).clean() + + # TODO: More useful error if domain is invalid? Currently we rely on designate schema check failing + + if not cleaned_data['backendPort'].isdigit() or int(cleaned_data['backendPort']) > 65535: + self._errors['backendPort'] = self.error_class([_('Enter a valid port')]) + + return cleaned_data + + def handle(self, request, data): + proxyip = socket.gethostbyname(urlparse.urlparse(base.url_for(request, 'proxy')).hostname) + if data.get('domain') == 'wmflabs.org.': + auth = identity_generic.Password( + auth_url=base.url_for(request, 'identity'), + username=getattr(settings, "WMFLABSDOTORG_ADMIN_USERNAME", ''), + password=getattr(settings, "WMFLABSDOTORG_ADMIN_PASSWORD", ''), + tenant_name='wmflabsdotorg', + user_domain_id='default', + project_domain_id='default' + ) + c = designateclientv2.Client(session=keystone_session.Session(auth=auth)) + + LOG.warn('Got create client') + # Create the record in the wmflabsdotorg project. This is needed since wmflabs.org lives + # in that project and designate prevents subdomain creation elsewhere. + zoneid = None + for zone in c.zones.list(): + if zone['name'] == 'wmflabs.org.': + zoneid = zone['id'] + break + else: + raise Exception("No zone ID") + LOG.warn('Got zone ID') + c.recordsets.create(zoneid, data.get('record') + '.wmflabs.org.', 'A', [proxyip]) + else: + # TODO: Move this to designate v2 API, reuse some code + c = designateapi.designateclient(request) + domainid = None + for domain in c.domains.list(): + if domain.name == data.get('domain'): + domainid = domain.id + break + else: + raise Exception("No domain ID") + record = Record(name=data.get('record') + '.' + data.get('domain'), type='A', data=proxyip) + c.records.create(domainid, record) + + d = { + "backends": [data.get('backendInstance') + ':' + data.get('backendPort')], + "domain": data.get('record') + '.' + data.get('domain') + } + + try: + resp = requests.put(base.url_for(request, 'proxy') + '/mapping', data=json.dumps(d)) + if resp: + return True + else: + raise Exception("Got status: " + resp.status_code) + except Exception: + exceptions.handle(self.request, _("Unable to create proxy: " + resp.text)) + return False + + +class CreateView(forms.ModalFormView): + form_class = CreateProxyForm + form_id = "create_proxy_form" + modal_header = _("Create a Proxy") + submit_label = _("Create Proxy") + submit_url = reverse_lazy('horizon:project:proxy:create') + template_name = 'project/proxy/create.html' + context_object_name = 'proxy' + success_url = reverse_lazy("horizon:project:proxy:index") + page_title = _("Create a Proxy") + + def get_initial(self): + initial = {} + for name in ['record', 'domain', 'backendInstance', 'backendPort']: + tmp = self.request.GET.get(name) + if tmp: + initial[name] = tmp + return initial diff --git a/modules/openstack/files/liberty/horizon/proxy_enable.py b/modules/openstack/files/liberty/horizon/proxy_enable.py new file mode 100644 index 0000000..c9919f8 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/proxy_enable.py @@ -0,0 +1,4 @@ +PANEL = 'proxy' +PANEL_GROUP = 'default' +PANEL_DASHBOARD = 'project' +ADD_PANEL = ('wikimediaproxydashboard.panel.Proxy') diff --git a/modules/openstack/manifests/horizon/service.pp b/modules/openstack/manifests/horizon/service.pp index b15f738..b8fda72 100644 --- a/modules/openstack/manifests/horizon/service.pp +++ b/modules/openstack/manifests/horizon/service.pp @@ -175,6 +175,23 @@ require => Package['python-designate-dashboard', 'openstack-dashboard'], } + # Proxy panel + file { '/usr/lib/python2.7/dist-packages/wikimediaproxydashboard': + source => "puppet:///modules/openstack/${openstack_version}/horizon/proxy", + owner => 'root', + group => 'root', + mode => '0644', + require => Package['python-designate-dashboard', 'openstack-dashboard'], + recurse => true + } + file { '/usr/share/openstack-dashboard/openstack_dashboard/local/enabled/_1922_project_proxy_panel.py': + source => "puppet:///modules/openstack/${openstack_version}/horizon/proxy_enable.py", + owner => 'root', + group => 'root', + mode => '0644', + require => Package['python-designate-dashboard', 'openstack-dashboard'], + } + # Monkeypatches for Horizon customization file { '/usr/lib/python2.7/dist-packages/horizon/overrides.py': source => "puppet:///modules/openstack/${openstack_version}/horizon/overrides.py", -- To view, visit https://gerrit.wikimedia.org/r/278871 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I785bf87a58c9361c9c92e51c2df64215665733fa Gerrit-PatchSet: 10 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Alex Monk <[email protected]> Gerrit-Reviewer: Alex Monk <[email protected]> Gerrit-Reviewer: Andrew Bogott <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
