Andrew Bogott has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/333987 )
Change subject: Horizon: Add mitaka version of the puppetpanel. ...................................................................... Horizon: Add mitaka version of the puppetpanel. No doubt this will require some future tweaking. Change-Id: I916270346bf4eed9bea1ff0dfe430379bb66267e --- A modules/openstack/files/mitaka/horizon/puppettab/__init__.py A modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/__init__.py A modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/plustab.py A modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/prefixpanel.py A modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/urls.py A modules/openstack/files/mitaka/horizon/puppettab/projectpanel.py A modules/openstack/files/mitaka/horizon/puppettab/puppet_config.py A modules/openstack/files/mitaka/horizon/puppettab/puppet_roles.py A modules/openstack/files/mitaka/horizon/puppettab/puppet_tables.py A modules/openstack/files/mitaka/horizon/puppettab/static/dashboard/puppet/puppet.scss A modules/openstack/files/mitaka/horizon/puppettab/tab.py A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_apply.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_detail_puppet.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_edithiera.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_editotherclasses.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_hiera.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_other_classes.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_remove.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_removeprefix.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/apply.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/edithiera.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/editotherclasses.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/plus_tab.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/prefix_panel.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/project_panel.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/remove.html A modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/removeprefix.html A modules/openstack/files/mitaka/horizon/puppettab/urls.py A modules/openstack/files/mitaka/horizon/puppettab/views.py 29 files changed, 1,426 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/operations/puppet refs/changes/87/333987/1 diff --git a/modules/openstack/files/mitaka/horizon/puppettab/__init__.py b/modules/openstack/files/mitaka/horizon/puppettab/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/__init__.py diff --git a/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/__init__.py b/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/__init__.py diff --git a/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/plustab.py b/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/plustab.py new file mode 100644 index 0000000..73b1776 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/plustab.py @@ -0,0 +1,64 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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 +import re + +from django import template +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ + +from wikimediapuppettab.puppet_config import puppet_config + +from horizon import exceptions +from horizon import tabs + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class PlusTab(tabs.Tab): + name = _("+") + slug = "puppetprefixplus" + template_name = "project/puppet/plus_tab.html" + prefix_name = False + + def __init__(self, *args, **kwargs): + if 'tenant_id' in kwargs: + self.tenant_id = kwargs['tenant_id'] + del kwargs['tenant_id'] + + super(PlusTab, self).__init__(*args, **kwargs) + + def render(self): + context = template.RequestContext(self.request) + context['prefix_name'] = self.prefix_name + return render_to_string(self.get_template_name(self.request), + self.data, context_instance=context) + + def post(self, request, *args, **kwargs): + + pattern = re.compile("^[A-Za-z][A-Za-z0-9_-]*$") + if not pattern.match(request.POST["prefix_name"]): + raise exceptions.BadRequest('Prefix must begin with a ' + 'letter and contain only letters, ' + 'numbers, _ or -.') + + self.prefix_name = request.POST["prefix_name"] + + # set an empty role list for this prefix, to force a + # record creation on the back end. + config = puppet_config(self.prefix_name, self.tenant_id) + config.set_role_list([]) diff --git a/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/prefixpanel.py b/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/prefixpanel.py new file mode 100644 index 0000000..40b2fd6 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/prefixpanel.py @@ -0,0 +1,91 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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 collections import OrderedDict +from django.utils.translation import ugettext_lazy as _ +import horizon +import logging + +from horizon import tabs +from horizon.tabs import TabGroup + +from wikimediapuppettab.tab import PuppetTab +from wikimediapuppettab.prefixpanel.plustab import PlusTab +from wikimediapuppettab.puppet_config import puppet_config + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class PrefixPuppetPanel(horizon.Panel): + name = _("Prefix Puppet") + slug = "prefixpuppet" + + +class PrefixTabs(tabs.TabGroup): + slug = "prefix_puppet" + sticky = False + + def __init__(self, request, **kwargs): + super(TabGroup, self).__init__() + + self.request = request + self.kwargs = kwargs + self._data = None + self.request = request + + self.tenant_id = self.request.user.tenant_id + self._tabs = OrderedDict(self.get_dynamic_tab_list()) + if self.sticky: + self.attrs['data-sticky-tabs'] = 'sticky' + if not self._set_active_tab(): + self.tabs_not_available() + + def get_dynamic_tab_list(self): + prefixlist = puppet_config.get_prefixes(self.tenant_id) + LOG.warning("prefixlist: %s" % prefixlist) + + tab_instances = [] + # One tab per prefix + for prefix in prefixlist: + # exclude anything with a '.' as those are instance names + # handled on a different UI + if '.' in prefix: + continue + if prefix == '_': + continue + tab_instances.append(("puppet-%s" % prefix, + PuppetTab(self, + self.request, + prefix=prefix, + tenant_id=self.tenant_id))) + + # + tab + tab_instances.append(('puppetprefixplus', + PlusTab(self, self.request, + tenant_id=self.tenant_id))) + return tab_instances + + def load_tab_data(self): + # This ensures that the tab list is updated without + # having to rebuild the whole object. + self._tabs = OrderedDict(self.get_dynamic_tab_list()) + + +class IndexView(tabs.TabbedTableView): + tab_group_class = PrefixTabs + template_name = 'project/puppet/prefix_panel.html' + page_title = _("Prefix Puppet") diff --git a/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/urls.py b/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/urls.py new file mode 100644 index 0000000..7786188 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/prefixpanel/urls.py @@ -0,0 +1,38 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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, patterns + +from wikimediapuppettab.prefixpanel import prefixpanel +from wikimediapuppettab import views + +urlpatterns = patterns( + '', + url(r'^$', prefixpanel.IndexView.as_view(), name='index'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + '(?P<roleid>[^/]+)/applypuppetrole$', + views.ApplyRoleView.as_view(), name='applypuppetrole'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + '(?P<roleid>[^/]+)/removepuppetrole$', + views.RemoveRoleView.as_view(), name='removepuppetrole'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + 'edithiera$', + views.EditHieraView.as_view(), name='edithiera'), + url(r'^(?P<tenantid>[^/]+)/' + 'newprefix$', + prefixpanel.IndexView.as_view(), name='newprefix'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + 'removepuppetprefix$', + views.RemovePrefixView.as_view(), name='removepuppetprefix'), +) diff --git a/modules/openstack/files/mitaka/horizon/puppettab/projectpanel.py b/modules/openstack/files/mitaka/horizon/puppettab/projectpanel.py new file mode 100644 index 0000000..97cca49 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/projectpanel.py @@ -0,0 +1,63 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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.utils.translation import ugettext_lazy as _ +import horizon + +from horizon import tabs + +import openstack_dashboard.dashboards.project.instances.tabs as instancetabs +from wikimediapuppettab.tab import PuppetTab + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class ProjectPuppetPanel(horizon.Panel): + name = _("Project Puppet") + slug = "puppet" + + @staticmethod + def can_register(): + # Hacky hook + # While we're here, add tabs to the instance detail view as well + instancetabs.InstanceDetailTabs.tabs += (PuppetTab,) + return True + + +class ProjectTabs(tabs.TabGroup): + slug = "project_puppet" + tabs = (PuppetTab, ) + sticky = True + + +class IndexView(tabs.TabbedTableView): + tab_group_class = ProjectTabs + template_name = 'project/puppet/project_panel.html' + page_title = _("Project Puppet") + + def get_tabs(self, request, *args, **kwargs): + if self._tab_group is None: + tenant_id = self.request.user.tenant_id + caption = _("These puppet settings will affect all VMs" + " in the %s project.") % tenant_id + self._tab_group = self.tab_group_class(request, + prefix='_', + caption=caption, + tenant_id=tenant_id, + **kwargs) + return self._tab_group diff --git a/modules/openstack/files/mitaka/horizon/puppettab/puppet_config.py b/modules/openstack/files/mitaka/horizon/puppettab/puppet_config.py new file mode 100644 index 0000000..1565e93 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/puppet_config.py @@ -0,0 +1,208 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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 +import requests +import puppet_roles +import yaml + +from django.conf import settings + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +# Get/set puppet config for a given instance. +# +# This class manages all communication with the home-made puppet REST backend +class puppet_config(): + def __init__(self, prefix, tenant_id): + self.prefix = prefix + self.tenant_id = tenant_id + self.apiurl = getattr(settings, + "PUPPET_CONFIG_BACKEND", + "http://labcontrol1001.wikimedia.org:8100/v1" + ) + self.refresh() + + def refresh(self): + classesurl = "%s/%s/prefix/%s/roles" % (self.apiurl, + self.tenant_id, + self.prefix) + req = requests.get(classesurl, verify=False) + if req.status_code == 404: + self.roles = [] + else: + req.raise_for_status() + self.roles = yaml.safe_load(req.text)['roles'] + + hieraurl = "%s/%s/prefix/%s/hiera" % (self.apiurl, + self.tenant_id, + self.prefix) + req = requests.get(hieraurl, verify=False) + if req.status_code == 404: + # Missing is the same as empty + self.hiera_raw = "{}" + else: + req.raise_for_status() + self.hiera_raw = yaml.safe_load(req.text)['hiera'] + + hiera_yaml = yaml.safe_load(self.hiera_raw) + if not hiera_yaml: + hiera_yaml = {} + self.role_dict = {} + + self.allroles = puppet_roles.available_roles() + allrole_dict = {role.name: role for role in self.allroles} + + # other_classes is a list of roles that weren't enumerated by the puppet API. + # These could be roles from a locally hacked puppet repo, or roles that have been + # deleted from the puppet repo but still appear in the instance config. + self.other_classes = [] + + # Find the hiera lines that assign params to applied and known roles. + # these lines are removed from the hiera text and added as + # structured data to the role records instead. + for role in list(self.roles): + if role in allrole_dict: + self.role_dict[role] = {} + for key in hiera_yaml.keys(): + if key.startswith(role): + # (len(role)+2) is the length of the rolename plus the ::, + # getting us the raw param name + argname = key[(len(role)+2):] + if hiera_yaml[key]: + self.role_dict[role][argname] = hiera_yaml[key] + del hiera_yaml[key] + allrole_dict[role].mark_applied(self.role_dict[role]) + elif role: + # This is an unknown role. Don't try to structure anything, just + # add the rolename to the list and let hiera take care of the + # params. + self.other_classes.append(role) + self.roles.remove(role) + else: + # Sometimes we get empty strings from crappy parsing + self.roles.remove(role) + + self.other_classes_text = "\n".join(self.other_classes) + + # Move the applied roles to the top for UI clarity + self.allroles.sort(key=lambda x: x.applied, reverse=True) + + self.hiera = yaml.safe_dump(hiera_yaml, default_flow_style=False) + + def remove_role(self, role): + if not self.roles: + LOG.error("Internal role list is empty, cannot remove") + # TODO throw an exception + return False + + roles = self.roles + + # Remove this role from our role list + roles.remove(role.name) + + # Remove all related role args from hiera + hiera_yaml = yaml.safe_load(self.hiera_raw) + if hiera_yaml: + for key in hiera_yaml.keys(): + if key.startswith("%s::" % role.name): + del hiera_yaml[key] + + self.set_role_list(roles) + self.set_hiera(hiera_yaml) + + def apply_role(self, role, params): + if not self.roles: + # this is the first role, so build a fresh list + roles = [role.name] + else: + roles = list(self.roles) + if role.name not in roles: + roles.append(role.name) + + # Translate the structured params and values + # into hiera yaml + hiera = yaml.safe_load(self.hiera_raw) + for param in params.keys(): + fullparam = "%s::%s" % (role.name, param) + if fullparam in hiera: + if params[param]: + hiera[fullparam] = params[param] + else: + del hiera[fullparam] + else: + if params[param]: + hiera[fullparam] = params[param] + + self.set_role_list(roles) + self.set_hiera(hiera) + + def set_roles(self, roles): + list_dump = yaml.safe_dump(roles, default_flow_style=False) + roleurl = "%s/%s/prefix/%s/roles" % (self.apiurl, + self.tenant_id, + self.prefix) + req = requests.post(roleurl, + verify=False, + data=list_dump, + headers={'Content-Type': 'application/x-yaml'}) + req.raise_for_status() + self.refresh() + + def set_other_class_list(self, other_class_list): + self.set_roles(other_class_list + self.roles) + + def set_role_list(self, role_list): + self.set_roles(role_list + self.other_classes) + + def set_hiera(self, hiera_yaml): + if not hiera_yaml: + # The user probably cleared the field. That's fine, we'll just + # convert that to {} + hiera_yaml = {} + hiera_dump = yaml.safe_dump(hiera_yaml, default_flow_style=False) + hieraurl = "%s/%s/prefix/%s/hiera" % (self.apiurl, + self.tenant_id, + self.prefix) + req = requests.post(hieraurl, + verify=False, + data=hiera_dump, + headers={'Content-Type': 'application/x-yaml'}) + req.raise_for_status() + self.refresh() + + @staticmethod + def delete_prefix(tenant_id, prefix): + apiurl = getattr(settings, + "PUPPET_CONFIG_BACKEND", + "http://labcontrol1001.wikimedia.org:8100/v1") + prefixurl = "%s/%s/prefix/%s" % (apiurl, tenant_id, prefix) + req = requests.delete(prefixurl, verify=False) + req.raise_for_status() + + @staticmethod + def get_prefixes(tenant_id): + apiurl = getattr(settings, + "PUPPET_CONFIG_BACKEND", + "http://labcontrol1001.wikimedia.org:8100/v1") + prefixurl = "%s/%s/prefix" % (apiurl, tenant_id) + req = requests.get(prefixurl, verify=False) + if req.status_code == 404: + return [] + else: + req.raise_for_status() + return yaml.safe_load(req.text)['prefixes'] diff --git a/modules/openstack/files/mitaka/horizon/puppettab/puppet_roles.py b/modules/openstack/files/mitaka/horizon/puppettab/puppet_roles.py new file mode 100644 index 0000000..11c72a8 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/puppet_roles.py @@ -0,0 +1,144 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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 +import requests + +from django.conf import settings +from django.core.cache import cache +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +# A single puppet class or role, used as the data type +# for our Horizon table-of-roles UI +class PuppetClass(): + name = None + html_name = "" + docs = "" + applied = False + params = [] + formatted_params = "" + raw_params = {} + filter_tags = [] + instance = None + + def __init__(self, name): + self.name = name + self.html_name = "" + self.docs = _('(No docs available)') + self.applied = False + self.params = [] + self.formatted_params = "" + self.raw_params = {} + self.filter_tags = [] + self.instance = None + + def update_prefix_data(self, prefix, tenant_id): + self.prefix = prefix + self.tenant_id = tenant_id + return self + + def mark_applied(self, paramdict): + self.applied = True + self.params = paramdict + if paramdict: + keysanddefaults = [] + for param in self.params.items(): + keysanddefaults.append("%s: %s" % param) + self.formatted_params = ";\n".join(keysanddefaults) + + return self + + +# Query the puppetmaster for a list of all available roles, +# build a list of PuppetClass() objects out of those roles. +# +# This list should be fairly static and building it is +# expensive, so it's cached in memcache. Local copies +# of this list will get altered with runtime data (e.g. +# tenant and instance information) but the cached version +# should remain useful universally. +def available_roles(): + key = 'wikimediapuppet_available_roles' + roles = cache.get(key) + if not roles: + apiurl = getattr(settings, + "PUPPETMASTER_API", + "https://labcontrol1001.wikimedia.org:8140/puppet" + ) + roleurl = "%s/resource_types/role" % apiurl + req = requests.get(roleurl, verify=False) + req.raise_for_status() + roleres = req.json() + + profileurl = "%s/resource_types/profile" % apiurl + req = requests.get(profileurl, verify=False) + req.raise_for_status() + profileres = req.json() + + res = roleres + profileres + + roles = [] + for role in res: + if role['kind'] != 'class': + continue + obj = PuppetClass(role['name']) + if 'doc' in role: + obj.docs = role['doc'] + if 'parameters' in role: + obj.params = role['parameters'] + obj.raw_params = role['parameters'] + keysanddefaults = [] + for param in obj.params.items(): + keysanddefaults.append("%s: %s" % param) + obj.formatted_params = ";\n".join(keysanddefaults) + + if 'doc' in role and (role['doc'].find('filtertags') != -1): + # Collect filter tags from the role comment, + # and generate 'newdoc' which is the docs without + # the filter line. + newdoc = "" + for line in role['doc'].splitlines(): + n = line.find('filtertags') + if n != -1: + obj.filter_tags = line[(n+11):].split() + else: + newdoc += "%s\n" % line + obj.docs = newdoc + + html = '<span title="%s">%s</>' % ( + escape(obj.docs), + escape(obj.name) + ) + obj.html_name = mark_safe(html) + + roles.append(obj) + + cache.set(key, roles, 300) + + return roles + + +def get_role_by_name(name): + allRoles = available_roles() + for role in allRoles: + if role.name == name: + return role + return None diff --git a/modules/openstack/files/mitaka/horizon/puppettab/puppet_tables.py b/modules/openstack/files/mitaka/horizon/puppettab/puppet_tables.py new file mode 100644 index 0000000..8f641c7 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/puppet_tables.py @@ -0,0 +1,137 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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 collections import defaultdict +import logging + +from django.core import urlresolvers +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class RemoveRole(tables.LinkAction): + name = 'remove' + verbose_name = _("Remove Class") + classes = ("ajax-modal",) + data_type_singular = _("Role") + + policy_rules = (("compute", "compute:delete"),) + + def get_link_url(self, datum): + url = "horizon:project:puppet:removepuppetrole" + kwargs = { + 'prefix': datum.prefix, + 'tenantid': datum.tenant_id, + 'roleid': datum.name, + } + return urlresolvers.reverse(url, kwargs=kwargs) + + def allowed(self, request, record=None): + return record.applied + + +class ApplyRole(tables.LinkAction): + name = 'apply_role' + verbose_name = _("Apply Class") + classes = ("ajax-modal",) + icon = "plus" + policy_rules = (("compute", "compute:delete"),) + + def get_link_url(self, datum): + url = "horizon:project:puppet:applypuppetrole" + kwargs = { + 'prefix': datum.prefix, + 'tenantid': datum.tenant_id, + 'roleid': datum.name, + } + return urlresolvers.reverse(url, kwargs=kwargs) + + def allowed(self, request, record=None): + return (not record.applied) + + +def get_categories_for_role(role): + categories = set(['all']) + if 'labs-common' in role.filter_tags: + categories.add('common') + if "labs-project-%s" % role.tenant_id in role.filter_tags: + categories.add('project') + return categories + + +# This shouldn't be needed, but FixedFilterAction +# is a bit broken and doesn't update unless we +# explicitly set those category-* classes. +class UpdateRow(tables.Row): + ajax = True + + def load_cells(self, role=None): + super(UpdateRow, self).load_cells(role) + # Tag the row with the image category for client-side filtering. + for cat in get_categories_for_role(self.datum): + self.classes.append('category-%s' % cat) + + +class RoleFilter(tables.FixedFilterAction): + def get_fixed_buttons(self): + def make_dict(text, tenant, icon): + return dict(text=text, value=tenant, icon=icon) + + buttons = [make_dict(_('project'), 'project', 'fa-star'), + make_dict(_('common'), 'common', 'fa-cube'), + make_dict(_('all'), 'all', 'fa-cubes')] + return buttons + + def categorize(self, table, roles): + filtered_dict = defaultdict(list) + for role in roles: + categories = get_categories_for_role(role) + for cat in categories: + filtered_dict[cat].append(role) + return filtered_dict + + +class PuppetTable(tables.DataTable): + applied = tables.Column('applied', verbose_name=_('Applied'), status=True) + name = tables.Column('html_name', + verbose_name=_('Name')) + params = tables.Column('formatted_params', + verbose_name=_('Parameters'), + sortable=False) + instance = tables.Column('instance', + verbose_name=_('Instance'), + hidden=True) + tenant = tables.Column('tenant', + verbose_name=_('Tenant'), + hidden=True) + tenant = tables.Column('prefix', + verbose_name=_('prefix'), + hidden=True) + roleid = tables.Column('name', verbose_name=_('ID'), hidden=True) + + class Meta(object): + name = 'puppet' + row_actions = (ApplyRole, RemoveRole) + table_actions = (RoleFilter,) + status_columns = ["applied"] + row_class = UpdateRow + multi_select = False + + def get_object_id(self, datum): + return datum.name diff --git a/modules/openstack/files/mitaka/horizon/puppettab/static/dashboard/puppet/puppet.scss b/modules/openstack/files/mitaka/horizon/puppettab/static/dashboard/puppet/puppet.scss new file mode 100644 index 0000000..97e7174 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/static/dashboard/puppet/puppet.scss @@ -0,0 +1 @@ +#puppet.table-striped tbody tr.status_up td {background:lightgreen} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/tab.py b/modules/openstack/files/mitaka/horizon/puppettab/tab.py new file mode 100644 index 0000000..e137e86 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/tab.py @@ -0,0 +1,140 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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.core import urlresolvers +from django.utils.translation import ugettext_lazy as _ + +from horizon import tabs +from django.conf import settings +from django.utils.safestring import mark_safe + +import puppet_tables as p_tables +from puppet_config import puppet_config + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class PuppetTab(tabs.TableTab): + name = _("Puppet Configuration") + slug = "puppet" + table_classes = (p_tables.PuppetTable,) + template_name = "project/puppet/_detail_puppet.html" + preload = False + + def __init__(self, *args, **kwargs): + # For some reason our parent class can't deal with these + # args, so extract them now if they're present + if 'prefix' in kwargs: + self.prefix = kwargs['prefix'] + self.name = self.prefix + del kwargs['prefix'] + + if 'tenant_id' in kwargs: + self.tenant_id = kwargs['tenant_id'] + del kwargs['tenant_id'] + + if hasattr(self, 'tenant_id') and hasattr(self, 'prefix'): + self.slug += '-%s' % self.prefix + self.tab_type = 'prefix' + + super(PuppetTab, self).__init__(*args, **kwargs) + + if 'instance' in self.tab_group.kwargs: + self.tab_type = 'instance' + tld = getattr(settings, + "INSTANCE_TLD", + "eqiad.wmflabs") + self.instance = self.tab_group.kwargs['instance'] + + self.prefix = "%s.%s.%s" % (self.instance.name, + self.instance.tenant_id, tld) + self.tenant_id = self.instance.tenant_id + + elif 'tenant_id' in self.tab_group.kwargs: + self.tab_type = 'project' + self.tenant_id = self.tab_group.kwargs['tenant_id'] + self.prefix = self.tab_group.kwargs['prefix'] + else: + self.tab_type = 'prefix' + + self.add_caption() + + self.config = puppet_config(self.prefix, self.tenant_id) + + def add_caption(self): + self.capption = "" + if self.tab_type == 'prefix': + self.caption = _("These puppet settings will affect all VMs in the" + " %s project whose names begin with \'%s\'.") % ( + self.tenant_id, self.prefix) + + elif self.tab_type == 'project': + self.caption = _("These puppet settings will affect all VMs" + " in the %s project.") % self.tenant_id + + elif self.tab_type == 'instance': + prefixes = puppet_config.get_prefixes(self.tenant_id) + links = [] + for prefix in prefixes: + if '.' in prefix: + continue + if prefix == '_': + links.append("<a href=\"%s\">project config</a>" % + urlresolvers.reverse( + "horizon:project:puppet:index")) + elif self.instance.name.startswith(prefix): + prefix_url = urlresolvers.reverse( + "horizon:project:prefixpuppet:index", + ) + "?tab=prefix_puppet__puppet-%s" % prefix + links.append("<a href=\"%s\">%s</a>" % (prefix_url, + prefix)) + + if links: + self.caption = mark_safe(_("This instance is also " + "affected by the following puppet " + "configs: %s" % ", ".join(links))) + + def get_context_data(self, request, **kwargs): + context = super(PuppetTab, self).get_context_data(request, **kwargs) + context['prefix'] = self.prefix + context['config'] = self.config + context['prefix_tab'] = (self.tab_type == 'prefix') + + if hasattr(self, 'caption'): + context['caption'] = self.caption + elif 'caption' in self.tab_group.kwargs: + context['caption'] = self.tab_group.kwargs['caption'] + + kwargs = { + 'prefix': self.prefix, + 'tenantid': self.tenant_id, + } + context['edithieraurl'] = urlresolvers.reverse( + "horizon:project:puppet:edithiera", kwargs=kwargs) + context['editotherclassesurl'] = urlresolvers.reverse( + "horizon:project:puppet:editotherclasses", kwargs=kwargs) + + url = "horizon:project:puppet:removepuppetprefix" + context['removepuppetprefixurl'] = urlresolvers.reverse(url, + kwargs=kwargs) + + return context + + def get_puppet_data(self): + return [role.update_prefix_data(self.prefix, self.tenant_id) for + role in self.config.allroles] diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_apply.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_apply.html new file mode 100644 index 0000000..570138e --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_apply.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-header %}{% trans "Apply" %} {{ puppetrole.name }} {% endblock %} + +{% block modal-body %} +<div class="row"> + <div class="col-sm-6" style="width: {% if puppetrole.docs %}38{% else %}100{% endif %}%"> + <h3>{{ ParamsCaption }}</h3> + <fieldset> + {% include "horizon/common/_form_fields.html" %} + </fieldset> + </div> + {% if puppetrole.docs %} + <div class="col-sm-6" style="width: 62%;"> + <h3>{{ DocsCaption }}</h3> + <p> + <font size="1"><pre>{{ puppetrole.docs }}</pre></font> + </p> + </div> + {% endif %} +</div> +{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_detail_puppet.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_detail_puppet.html new file mode 100644 index 0000000..ce53ad3 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_detail_puppet.html @@ -0,0 +1,17 @@ +{% load i18n %} + +<br> +{{ caption }} +{% if prefix_tab %} +<br> +<a href="{{ removepuppetprefixurl }}" class="btn btn-primary ajax-modal">{% trans "Remove prefix" %}</a> +{% endif %} + +<div class="row-fluid"> + <div class="span12"> + <h3>{% trans "Roles and Profiles" %}</h3> + {{ table.render }} + </div> + {% include 'project/puppet/_other_classes.html' %} + {% include 'project/puppet/_hiera.html' %} +</div> diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_edithiera.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_edithiera.html new file mode 100644 index 0000000..b9d6be2 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_edithiera.html @@ -0,0 +1,4 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_editotherclasses.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_editotherclasses.html new file mode 100644 index 0000000..ffbc95a --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_editotherclasses.html @@ -0,0 +1,3 @@ +{% extends "horizon/common/_modal_form.html" %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_hiera.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_hiera.html new file mode 100644 index 0000000..7a89c64 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_hiera.html @@ -0,0 +1,10 @@ +{% load i18n %} + +<div class="row-fluid"> + <h3>{% trans "Hiera Config" %}</h3> + <p> + <pre>{{ config.hiera }}</pre> + </p> + <a href="{{ edithieraurl }}" class="btn btn-primary ajax-modal">{% trans "Edit" %}</a> +</div> + diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_other_classes.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_other_classes.html new file mode 100644 index 0000000..1c4099d --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_other_classes.html @@ -0,0 +1,9 @@ +{% load i18n %} + +<div class="row-fluid"> + <h3>{% trans "Other Classes" %}</h3> + <p> + <pre>{{ config.other_classes_text }}</pre> + </p> + <a href="{{ editotherclassesurl }}" class="btn btn-primary ajax-modal">{% trans "Edit" %}</a> +</div> diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_remove.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_remove.html new file mode 100644 index 0000000..b3c8816 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_remove.html @@ -0,0 +1,22 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-header %}{% trans "Remove" %} {{ puppetrole.name }} {% endblock %} + +{% block modal-body %} +<div class="row"> + <div class="col-sm-6" style="width: {% if puppetrole.docs %}38{% else %}100{% endif %}%"> + <h3>Are you sure you want to remove this role?</h3> + </div> + {% if puppetrole.docs %} + <div class="col-sm-6" style="width: 62%;"> + <h3>{{ DocsCaption }}</h3> + <p> + <font size="1"><pre>{{ puppetrole.docs }}</pre></font> + </p> + </div> + {% endif %} +</div> +{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_removeprefix.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_removeprefix.html new file mode 100644 index 0000000..00413aa --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/_removeprefix.html @@ -0,0 +1,15 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-header %}{% trans "Remove" %} {{ prefix }} {% endblock %} + +{% block modal-body %} +<div class="row"> + <div class="col-sm-6" style="width: 100%"> + <h3>{% trans "Are you sure you want to remove this prefix?" %}</h3> + {% trans "All hiera and puppet roles associated with this prefix will be discarded." %} + </div> +</div> +{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/apply.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/apply.html new file mode 100644 index 0000000..a4e7d38 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/apply.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Apply A Role" %}{% endblock %} + +{% block main %} + {% include 'project/puppet/_apply.html' %} +{% endblock %} + diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/edithiera.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/edithiera.html new file mode 100644 index 0000000..2416c95 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/edithiera.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block main %} + {% include 'project/puppet/_edithiera.html' %} +{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/editotherclasses.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/editotherclasses.html new file mode 100644 index 0000000..55b0a00 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/editotherclasses.html @@ -0,0 +1,5 @@ +{% extends 'base.html' %} + +{% block main %} + {% include 'project/puppet/_editotherclasses.html' %} +{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/plus_tab.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/plus_tab.html new file mode 100644 index 0000000..f5338a3 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/plus_tab.html @@ -0,0 +1,15 @@ +{% load i18n %} + +{% if prefix_name %} +Name: {{ prefix_name }} +{% else %} +<br> +{% trans "A prefix must start with a letter and consist of only letters, numbers, - and _." %} +<form action="" method="POST"> + {% csrf_token %} + <input type="hidden" name="action" value="puppetprefixplus__undefined"> + <input type="text" name="prefix_name" placeholder="prefix name:" pattern="^[A-Za-z][A-Za-z0-9-_]*$"> + <input type="submit" value="Add prefix"><br> +</form> +{% endif %} + diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/prefix_panel.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/prefix_panel.html new file mode 100644 index 0000000..6d74e64 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/prefix_panel.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %} + {{ page_title }} +{% endblock %} + +{% block main %} + <div class="row"> + <div class="col-sm-12"> + {{ tab_group.render }} + </div> + </div> +{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/project_panel.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/project_panel.html new file mode 100644 index 0000000..6d74e64 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/project_panel.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %} + {{ page_title }} +{% endblock %} + +{% block main %} + <div class="row"> + <div class="col-sm-12"> + {{ tab_group.render }} + </div> + </div> +{% endblock %} diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/remove.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/remove.html new file mode 100644 index 0000000..a8e6cd8 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/remove.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove A Role" %}{% endblock %} + +{% block main %} + {% include 'project/puppet/_remove.html' %} +{% endblock %} + diff --git a/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/removeprefix.html b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/removeprefix.html new file mode 100644 index 0000000..4ab70e4 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/templates/puppet/removeprefix.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove A Puppet Rule Prefix" %}{% endblock %} + +{% block main %} + {% include 'project/puppet/_remove.html' %} +{% endblock %} + diff --git a/modules/openstack/files/mitaka/horizon/puppettab/urls.py b/modules/openstack/files/mitaka/horizon/puppettab/urls.py new file mode 100644 index 0000000..190a9c7 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/urls.py @@ -0,0 +1,38 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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, patterns + +from wikimediapuppettab import projectpanel +from wikimediapuppettab import views + +urlpatterns = patterns( + '', + url(r'^$', projectpanel.IndexView.as_view(), name='index'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + '(?P<roleid>[^/]+)/applypuppetrole$', + views.ApplyRoleView.as_view(), name='applypuppetrole'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + '(?P<roleid>[^/]+)/removepuppetrole$', + views.RemoveRoleView.as_view(), name='removepuppetrole'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + 'edithiera$', + views.EditHieraView.as_view(), name='edithiera'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + 'editotherclasses$', + views.EditOtherClassesView.as_view(), name='editotherclasses'), + url(r'^(?P<prefix>[^/]+)/(?P<tenantid>[^/]+)/' + 'removepuppetprefix$', + views.RemovePrefixView.as_view(), name='removepuppetprefix'), +) diff --git a/modules/openstack/files/mitaka/horizon/puppettab/views.py b/modules/openstack/files/mitaka/horizon/puppettab/views.py new file mode 100644 index 0000000..1eaf027 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/puppettab/views.py @@ -0,0 +1,319 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# 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.core import urlresolvers +from django.core.validators import URLValidator +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms + +from puppet_config import puppet_config + +import puppet_roles + +import yaml + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class EditHieraForm(forms.SelfHandlingForm): + prefix = forms.CharField(widget=forms.HiddenInput()) + tenant_id = forms.CharField(widget=forms.HiddenInput()) + hieradata = forms.CharField(label=_("Instance hiera config:"), + widget=forms.Textarea(attrs={ + 'cols': 80, + 'rows': 15}), + required=False) + + def handle(self, request, data): + config = puppet_config(data['prefix'], data['tenant_id']) + config.set_hiera(yaml.safe_load(data['hieradata'])) + return True + + +class EditHieraView(forms.ModalFormView): + form_class = EditHieraForm + form_id = "edit_hiera_form" + modal_header = _("Edit Hiera") + submit_label = _("Apply Changes") + submit_url = "horizon:project:puppet:edithiera" + template_name = "project/puppet/edithiera.html" + context_object_name = 'hieraconfig' + + def get_context_data(self, **kwargs): + context = super(EditHieraView, self).get_context_data(**kwargs) + context['prefix'] = self.prefix + context['hieradata'] = self.hieradata.hiera + urlkwargs = { + 'prefix': self.prefix, + 'tenantid': self.tenant_id, + } + context['submit_url'] = urlresolvers.reverse(self.submit_url, + kwargs=urlkwargs) + return context + + def get_success_url(self): + validate = URLValidator() + refer = self.request.META.get('HTTP_REFERER', '/') + validate(refer) + return refer + + def get_prefix(self): + return self.kwargs['prefix'] + + def get_tenant_id(self): + return self.kwargs['tenantid'] + + def get_initial(self): + initial = {} + self.prefix = self.get_prefix() + self.tenant_id = self.get_tenant_id() + self.hieradata = puppet_config(self.prefix, self.tenant_id) + initial['hieradata'] = self.hieradata.hiera + initial['prefix'] = self.prefix + initial['tenant_id'] = self.tenant_id + + return initial + + +class RoleViewBase(forms.ModalFormView): + context_object_name = 'puppetrole' + + puppetrole_name = forms.CharField(widget=forms.HiddenInput()) + + def get_context_data(self, **kwargs): + context = super(RoleViewBase, self).get_context_data(**kwargs) + context['puppetrole'] = self.puppet_role + urlkwargs = { + 'prefix': self.prefix, + 'tenantid': self.tenant_id, + 'roleid': self.role_id, + } + context['prefix'] = self.prefix + context['submit_url'] = urlresolvers.reverse(self.submit_url, + kwargs=urlkwargs) + if self.puppet_role.docs: + context['DocsCaption'] = _('Description:') + else: + context['DocsCaption'] = _('(No Description)') + if self.puppet_role.params: + context['ParamsCaption'] = _('Parameters:') + else: + context['ParamsCaption'] = _('(No Parameters)') + return context + + def get_success_url(self): + validate = URLValidator() + refer = self.request.META.get('HTTP_REFERER', '/') + validate(refer) + return refer + + def get_puppet_role(self): + rolename = self.kwargs['roleid'] + puppet_role = puppet_roles.get_role_by_name(rolename) + return puppet_role + + def get_prefix(self): + return self.kwargs['prefix'] + + def get_tenant_id(self): + return self.kwargs['tenantid'] + + def get_initial(self): + initial = {} + self.prefix = self.get_prefix() + self.tenant_id = self.get_tenant_id() + self.role_id = self.kwargs['roleid'] + self.puppet_role = self.get_puppet_role() + initial['puppet_role'] = self.puppet_role + initial['tenant_id'] = self.tenant_id + initial['prefix'] = self.prefix + return initial + + +class ApplyRoleForm(forms.SelfHandlingForm): + def __init__(self, request, *args, **kwargs): + super(ApplyRoleForm, self).__init__(request, *args, **kwargs) + initial = kwargs.get('initial', {}) + self.tenant_id = initial['tenant_id'] + self.prefix = initial['prefix'] + self.role = initial['puppet_role'] + if self.role.params: + for key in self.role.params.keys(): + defaultval = self.role.params.get(key, '') + if defaultval: + defaultval = "default: %s" % defaultval + self.fields[key] = forms.CharField( + label=mark_safe("%s <i><small>%s</small></i>" % ( + key, + defaultval)), + required=False + ) + + def handle(self, request, data): + config = puppet_config(self.prefix, self.tenant_id) + config.apply_role(self.role, data) + return True + + +class ApplyRoleView(RoleViewBase): + form_class = ApplyRoleForm + form_id = "apply_role_form" + modal_header = _("Apply Class") + submit_label = _("Apply") + submit_url = "horizon:project:puppet:applypuppetrole" + template_name = "project/puppet/apply.html" + + +class RemoveRoleForm(forms.SelfHandlingForm): + def __init__(self, request, *args, **kwargs): + super(RemoveRoleForm, self).__init__(request, *args, **kwargs) + initial = kwargs.get('initial', {}) + self.tenant_id = initial['tenant_id'] + self.prefix = initial['prefix'] + self.role = initial['puppet_role'] + + def handle(self, request, data): + config = puppet_config(self.prefix, self.tenant_id) + config.remove_role(self.role) + return True + + +class RemoveRoleView(RoleViewBase): + form_class = RemoveRoleForm + form_id = "remove_role_form" + modal_header = _("Remove Class") + submit_label = _("Remove") + submit_url = "horizon:project:puppet:removepuppetrole" + template_name = "project/puppet/remove.html" + + +class RemovePrefixForm(forms.SelfHandlingForm): + def __init__(self, request, *args, **kwargs): + super(RemovePrefixForm, self).__init__(request, *args, **kwargs) + initial = kwargs.get('initial', {}) + self.tenant_id = initial['tenant_id'] + self.prefix = initial['prefix'] + + def handle(self, request, data): + puppet_config.delete_prefix(self.tenant_id, self.prefix) + return True + + +class RemovePrefixView(forms.ModalFormView): + form_class = RemovePrefixForm + form_id = "remove_prefix_form" + modal_header = _("Remove Prefix") + submit_label = _("Remove") + submit_url = "horizon:project:puppet:removepuppetprefix" + template_name = "project/puppet/removeprefix.html" + + def get_prefix(self): + return self.kwargs['prefix'] + + def get_tenant_id(self): + return self.kwargs['tenantid'] + + def get_initial(self): + initial = {} + self.prefix = self.get_prefix() + self.tenant_id = self.get_tenant_id() + initial['prefix'] = self.prefix + initial['tenant_id'] = self.tenant_id + + return initial + + def get_context_data(self, **kwargs): + context = super(RemovePrefixView, self).get_context_data(**kwargs) + context['prefix'] = self.prefix + urlkwargs = { + 'prefix': self.prefix, + 'tenantid': self.tenant_id, + } + context['prefix'] = self.prefix + context['submit_url'] = urlresolvers.reverse(self.submit_url, + kwargs=urlkwargs) + return context + + def get_success_url(self): + validate = URLValidator() + refer = self.request.META.get('HTTP_REFERER', '/') + validate(refer) + return refer + + +class EditOtherClassesForm(forms.SelfHandlingForm): + prefix = forms.CharField(widget=forms.HiddenInput()) + tenant_id = forms.CharField(widget=forms.HiddenInput()) + classes = forms.CharField(label=_("Other classes:"), + widget=forms.Textarea(attrs={ + 'cols': 80, + 'rows': 15}), + required=False) + + def handle(self, request, data): + other_class_list = [cls.strip() for cls in data['classes'].strip().split("\n") if cls] + config = puppet_config(data['prefix'], data['tenant_id']) + config.set_other_class_list(other_class_list) + return True + + +class EditOtherClassesView(forms.ModalFormView): + form_class = EditOtherClassesForm + form_id = "edit_otherclasses_form" + modal_header = _("Edit Other Classes") + submit_label = _("Apply Changes") + submit_url = "horizon:project:puppet:editotherclasses" + template_name = "project/puppet/editotherclasses.html" + context_object_name = 'otherclassesconfig' + + def get_context_data(self, **kwargs): + context = super(EditOtherClassesView, self).get_context_data(**kwargs) + context['prefix'] = self.prefix + context['classes'] = self.classdata.other_classes + urlkwargs = { + 'prefix': self.prefix, + 'tenantid': self.tenant_id, + } + context['submit_url'] = urlresolvers.reverse(self.submit_url, + kwargs=urlkwargs) + return context + + def get_success_url(self): + validate = URLValidator() + refer = self.request.META.get('HTTP_REFERER', '/') + validate(refer) + return refer + + def get_prefix(self): + return self.kwargs['prefix'] + + def get_tenant_id(self): + return self.kwargs['tenantid'] + + def get_initial(self): + initial = {} + self.prefix = self.get_prefix() + self.tenant_id = self.get_tenant_id() + self.classdata = puppet_config(self.prefix, self.tenant_id) + initial['classes'] = self.classdata.other_classes_text + initial['prefix'] = self.prefix + initial['tenant_id'] = self.tenant_id + + return initial -- To view, visit https://gerrit.wikimedia.org/r/333987 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I916270346bf4eed9bea1ff0dfe430379bb66267e Gerrit-PatchSet: 1 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Andrew Bogott <abog...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits