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

Reply via email to