From 561f3590a021e3f5e4888d99ef8b8aa2128b888e Mon Sep 17 00:00:00 2001 From: Graham Hayes Date: Wed, 26 Nov 2014 20:18:58 +0000 Subject: [PATCH] Added designate-dashboard code to contrib/designate-dashboard Added to devstack as well. Change-Id: I98eed46a8dd9918ba36c97fe40619ec42136c723 --- CONTRIBUTING.rst | 17 + HACKING.rst | 4 + LICENSE | 175 ++++++ MANIFEST.in | 6 + README.rst | 19 + babel.cfg | 1 + designatedashboard/__init__.py | 19 + designatedashboard/api/__init__.py | 1 + designatedashboard/api/designate.py | 161 ++++++ designatedashboard/dashboards/__init__.py | 0 .../dashboards/project/__init__.py | 0 .../project/dns_domains/__init__.py | 0 .../dashboards/project/dns_domains/forms.py | 519 ++++++++++++++++++ .../dashboards/project/dns_domains/panel.py | 26 + .../dashboards/project/dns_domains/tables.py | 193 +++++++ .../templates/dns_domains/_create_domain.html | 38 ++ .../templates/dns_domains/_create_record.html | 21 + .../templates/dns_domains/_update_domain.html | 36 ++ .../templates/dns_domains/_update_record.html | 21 + .../templates/dns_domains/create_domain.html | 11 + .../templates/dns_domains/create_record.html | 11 + .../templates/dns_domains/index.html | 11 + .../templates/dns_domains/records.html | 32 ++ .../templates/dns_domains/update_domain.html | 11 + .../templates/dns_domains/update_record.html | 11 + .../dashboards/project/dns_domains/tests.py | 447 +++++++++++++++ .../dashboards/project/dns_domains/urls.py | 44 ++ .../dashboards/project/dns_domains/views.py | 181 ++++++ designatedashboard/exceptions.py | 21 + designatedashboard/tests/.secret_key_store | 1 + designatedashboard/tests/__init__.py | 0 designatedashboard/tests/base.py | 53 ++ designatedashboard/tests/settings.py | 133 +++++ .../tests/test_designatedashboard.py | 448 +++++++++++++++ doc/source/conf.py | 75 +++ doc/source/contributing.rst | 4 + doc/source/index.rst | 24 + doc/source/installation.rst | 12 + doc/source/readme.rst | 1 + doc/source/usage.rst | 7 + enabled/_70_dns_add_group.py | 20 + enabled/_71_dns_project.py | 26 + openstack-common.conf | 6 + requirements.txt | 7 + setup.cfg | 46 ++ setup.py | 22 + test | Bin 0 -> 2048 bytes test-requirements.txt | 14 + tox.ini | 34 ++ 49 files changed, 2970 insertions(+) create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 babel.cfg create mode 100644 designatedashboard/__init__.py create mode 100644 designatedashboard/api/__init__.py create mode 100644 designatedashboard/api/designate.py create mode 100644 designatedashboard/dashboards/__init__.py create mode 100644 designatedashboard/dashboards/project/__init__.py create mode 100644 designatedashboard/dashboards/project/dns_domains/__init__.py create mode 100644 designatedashboard/dashboards/project/dns_domains/forms.py create mode 100644 designatedashboard/dashboards/project/dns_domains/panel.py create mode 100644 designatedashboard/dashboards/project/dns_domains/tables.py create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_create_domain.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_create_record.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_update_domain.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_update_record.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/create_domain.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/create_record.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/index.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/records.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/update_domain.html create mode 100644 designatedashboard/dashboards/project/dns_domains/templates/dns_domains/update_record.html create mode 100644 designatedashboard/dashboards/project/dns_domains/tests.py create mode 100644 designatedashboard/dashboards/project/dns_domains/urls.py create mode 100644 designatedashboard/dashboards/project/dns_domains/views.py create mode 100644 designatedashboard/exceptions.py create mode 100644 designatedashboard/tests/.secret_key_store create mode 100644 designatedashboard/tests/__init__.py create mode 100644 designatedashboard/tests/base.py create mode 100644 designatedashboard/tests/settings.py create mode 100644 designatedashboard/tests/test_designatedashboard.py create mode 100755 doc/source/conf.py create mode 100644 doc/source/contributing.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/installation.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/usage.rst create mode 100644 enabled/_70_dns_add_group.py create mode 100644 enabled/_71_dns_project.py create mode 100644 openstack-common.conf create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 test create mode 100644 test-requirements.txt create mode 100644 tox.ini diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..51e5d63 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps in the "If you're a developer, start here" +section of this page: + + http://wiki.openstack.org/HowToContribute + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://wiki.openstack.org/GerritWorkflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/designatedashboard \ No newline at end of file diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..e0ddef5 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +designatedashboard Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..90f8a7a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..baaf85c --- /dev/null +++ b/README.rst @@ -0,0 +1,19 @@ +=============================== +designatedashboard +=============================== + +Designate Horizon UI bits + +* Free software: Apache license + +Features +-------- + +* TODO + + +Howto +----- + +1. Clone Horizon UI folder +2. Symlink enabled/* files to openstack_dashboard/local/enabled folder and run horizon! diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..efceab8 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/designatedashboard/__init__.py b/designatedashboard/__init__.py new file mode 100644 index 0000000..45adf78 --- /dev/null +++ b/designatedashboard/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# 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 pbr.version + + +__version__ = pbr.version.VersionInfo( + 'designatedashboard').version_string() diff --git a/designatedashboard/api/__init__.py b/designatedashboard/api/__init__.py new file mode 100644 index 0000000..97ba638 --- /dev/null +++ b/designatedashboard/api/__init__.py @@ -0,0 +1 @@ +from designatedashboard.api import designate # noqa diff --git a/designatedashboard/api/designate.py b/designatedashboard/api/designate.py new file mode 100644 index 0000000..b3137b8 --- /dev/null +++ b/designatedashboard/api/designate.py @@ -0,0 +1,161 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 __future__ import absolute_import + +from horizon import exceptions +import logging + +from designateclient.v1 import Client # noqa +from designateclient.v1.domains import Domain # noqa +from designateclient.v1.records import Record # noqa + +from openstack_dashboard.api.base import url_for # noqa + +LOG = logging.getLogger(__name__) + + +def designateclient(request): + designate_url = "" + try: + designate_url = url_for(request, 'dns') + except exceptions.ServiceCatalogException: + LOG.debug('no dns service configured.') + return None + + LOG.debug('designateclient connection created using token "%s"' + 'and url "%s"' % (request.user.token.id, designate_url)) + + return Client(endpoint=designate_url, + token=request.user.token.id, + username=request.user.username, + tenant_id=request.user.project_id) + + +def domain_get(request, domain_id): + d_client = designateclient(request) + if d_client is None: + return [] + return d_client.domains.get(domain_id) + + +def domain_list(request): + d_client = designateclient(request) + if d_client is None: + return [] + return d_client.domains.list() + + +def domain_create(request, name, email, ttl=None, description=None): + d_client = designateclient(request) + if d_client is None: + return None + + options = { + 'description': description, + } + + # TTL needs to be optionally added as argument because the client + # won't accept a None value + if ttl is not None: + options['ttl'] = ttl + + domain = Domain(name=name, email=email, **options) + + return d_client.domains.create(domain) + + +def domain_update(request, domain_id, email, ttl, description=None): + d_client = designateclient(request) + if d_client is None: + return None + + # A quirk of the designate client is that you need to start with a + # base record and then update individual fields in order to persist + # the data. The designate client will only send the 'changed' fields. + domain = Domain(id=domain_id, name='', email='') + + domain.email = email + domain.ttl = ttl + domain.description = description + + return d_client.domains.update(domain) + + +def domain_delete(request, domain_id): + d_client = designateclient(request) + if d_client is None: + return [] + return d_client.domains.delete(domain_id) + + +def server_list(request, domain_id): + d_client = designateclient(request) + if d_client is None: + return [] + return d_client.domains.list_domain_servers(domain_id) + + +def record_list(request, domain_id): + d_client = designateclient(request) + if d_client is None: + return [] + return d_client.records.list(domain_id) + + +def record_get(request, domain_id, record_id): + d_client = designateclient(request) + if d_client is None: + return [] + return d_client.records.get(domain_id, record_id) + + +def record_delete(request, domain_id, record_id): + d_client = designateclient(request) + if d_client is None: + return [] + return d_client.records.delete(domain_id, record_id) + + +def record_create(request, domain_id, **kwargs): + d_client = designateclient(request) + if d_client is None: + return [] + + record = Record(**kwargs) + return d_client.records.create(domain_id, record) + + +def record_update(request, domain_id, record_id, **kwargs): + d_client = designateclient(request) + if d_client is None: + return [] + + # A quirk of the designate client is that you need to start with a + # base record and then update individual fields in order to persist + # the data. The designate client will only send the 'changed' fields. + record = Record( + id=record_id, + type='A', + name='', + data='') + + record.type = kwargs.get('type', None) + record.name = kwargs.get('name', None) + record.data = kwargs.get('data', None) + record.priority = kwargs.get('priority', None) + record.ttl = kwargs.get('ttl', None) + record.description = kwargs.get('description', None) + + return d_client.records.update(domain_id, record) diff --git a/designatedashboard/dashboards/__init__.py b/designatedashboard/dashboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designatedashboard/dashboards/project/__init__.py b/designatedashboard/dashboards/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designatedashboard/dashboards/project/dns_domains/__init__.py b/designatedashboard/dashboards/project/dns_domains/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designatedashboard/dashboards/project/dns_domains/forms.py b/designatedashboard/dashboards/project/dns_domains/forms.py new file mode 100644 index 0000000..8b8147a --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/forms.py @@ -0,0 +1,519 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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.core.exceptions import ValidationError # noqa +from django.core import validators +from django.utils.translation import ugettext_lazy as _ +from horizon import forms +from horizon import messages +from designatedashboard import api +from designateclient import exceptions as designate_exceptions + + +LOG = logging.getLogger(__name__) + +MAX_TTL = 2147483647 +# These regexes were given to me by Kiall Mac Innes here: +# https://gerrit.hpcloud.net/#/c/25300/2/ +DOMAIN_NAME_REGEX = r'^(?!.{255,})(?:(?!\-)[A-Za-z0-9_\-]{1,63}(?= 500: + self.api_error( + _('Error: %(message)s (Request ID: %(request_id)s)') + % { + "request_id": ex.request_id, + "message": ex.message + }) + else: + self.api_error( + _('Error: %(message)s') + % { + "message": ex.message + }) + + return False + + except Exception: + messages.error(request, _('Unable to create domain.')) + return True + + +class DomainUpdate(DomainForm): + + '''Form for displaying domain record details and updating them.''' + + id = forms.CharField( + required=False, + widget=forms.HiddenInput() + ) + + serial = forms.CharField( + label=_("Serial"), + required=False, + widget=forms.TextInput(attrs={'readonly': 'readonly'}), + ) + + created_at = forms.CharField( + label=_("Created At"), + required=False, + widget=forms.TextInput(attrs={'readonly': 'readonly'}), + ) + + updated_at = forms.CharField( + label=_("Updated At"), + required=False, + widget=forms.TextInput(attrs={'readonly': 'readonly'}), + ) + + def __init__(self, request, *args, **kwargs): + super(DomainUpdate, self).__init__(request, *args, **kwargs) + + # Mark name as read-only + self.fields['name'].required = False + self.fields['name'].widget.attrs['readonly'] = 'readonly' + + self.fields['ttl'].required = True + + # Customize display order for fields + self.fields.keyOrder = [ + 'id', + 'name', + 'serial', + 'email', + 'ttl', + 'description', + 'created_at', + 'updated_at', + ] + + def handle(self, request, data): + try: + domain = api.designate.domain_update( + request, + domain_id=data['id'], + email=data['email'], + ttl=data['ttl'], + description=data['description']) + messages.success(request, + _('Domain %(name)s updated.') % + {"name": domain.name}) + return domain + + except designate_exceptions.RemoteError as ex: + + if ex.code >= 500: + self.api_error( + _('Error: %(message)s (Request ID: %(request_id)s)') + % { + "request_id": ex.request_id, + "message": ex.message + }) + else: + self.api_error( + _('Error: %(message)s') + % { + "message": ex.message + }) + + return False + + except Exception: + messages.error(request, _('Unable to update domain.')) + return True + + +class RecordForm(forms.SelfHandlingForm): + + '''Base class for RecordCreate and RecordUpdate forms. + + Sets-up all of + the form fields and implements the complex validation logic. + ''' + + domain_id = forms.CharField( + widget=forms.HiddenInput()) + + domain_name = forms.CharField( + widget=forms.HiddenInput()) + + type = forms.ChoiceField( + label=_("Record Type"), + required=False, + choices=[ + ('a', _('A')), + ('aaaa', _('AAAA')), + ('cname', _('CNAME')), + ('mx', _('MX')), + ('ptr', _('PTR')), + ('spf', _('SPF')), + ('srv', _('SRV')), + ('sshfp', _('SSHFP')), + ('txt', _('TXT')), + ], + widget=forms.Select(attrs={ + 'class': 'switchable', + 'data-slug': 'record_type', + }), + ) + + name = forms.CharField( + max_length=256, + required=False, + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'record_type', + 'data-record_type-a': _('Name'), + 'data-record_type-aaaa': _('Name'), + 'data-record_type-cname': _('Name'), + 'data-record_type-mx': _('Name'), + 'data-record_type-ns': _('Name'), + 'data-record_type-ptr': _('Name'), + 'data-record_type-soa': _('Name'), + 'data-record_type-spf': _('Name'), + 'data-record_type-srv': _('Name'), + 'data-record_type-sshfp': _('Name'), + 'data-record_type-txt': _('Name'), + }), + ) + + data = forms.CharField( + required=False, + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'record_type', + 'data-record_type-a': _('IP Address'), + 'data-record_type-aaaa': _('IP Address'), + 'data-record_type-cname': _('Canonical Name'), + 'data-record_type-ns': _('Name Server'), + 'data-record_type-mx': _('Mail Server'), + 'data-record_type-ptr': _('PTR Domain Name'), + 'data-record_type-soa': _('Value'), + 'data-record_type-srv': _('Value'), + }), + ) + + txt = forms.CharField( + label=_('TXT'), + required=False, + widget=forms.Textarea(attrs={ + 'class': 'switched', + 'data-switch-on': 'record_type', + 'data-record_type-txt': _('Text'), + 'data-record_type-spf': _('Text'), + 'data-record_type-sshfp': _('Text'), + }), + ) + + priority = forms.IntegerField( + min_value=0, + max_value=65535, + required=False, + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'record_type', + 'data-record_type-mx': _('Priority'), + 'data-record_type-srv': _('Priority'), + }), + ) + + ttl = forms.IntegerField( + label=_('TTL'), + min_value=0, + max_value=MAX_TTL, + required=False, + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'record_type', + 'data-record_type-a': _('TTL'), + 'data-record_type-aaaa': _('TTL'), + 'data-record_type-cname': _('TTL'), + 'data-record_type-mx': _('TTL'), + 'data-record_type-ptr': _('TTL'), + 'data-record_type-soa': _('TTL'), + 'data-record_type-spf': _('TTL'), + 'data-record_type-srv': _('TTL'), + 'data-record_type-sshfp': _('TTL'), + 'data-record_type-txt': _('TTL'), + }), + ) + + description = forms.CharField( + label=_("Description"), + required=False, + max_length=160, + widget=forms.Textarea(), + ) + + def clean_type(self): + '''Type value needs to be uppercased before it is sent to the API.''' + return self.cleaned_data['type'].upper() + + def clean(self): + '''Handles the validation logic for the domain record form. + + Validation gets pretty complicated due to the fact that the different + record types (A, AAAA, MX, etc) have different requirements for + each of the fields. + ''' + + cleaned_data = super(RecordForm, self).clean() + record_type = cleaned_data['type'] + domain_name = cleaned_data['domain_name'] + + # Name field + if self._is_field_blank(cleaned_data, 'name'): + if record_type in ['A', 'AAAA', 'CNAME', 'SRV', 'TXT', 'PTR']: + self._add_required_field_error('name') + elif record_type == 'MX': + cleaned_data['name'] = domain_name + else: + if record_type == 'SRV': + if not re.match(SRV_NAME_REGEX, cleaned_data['name']): + self._add_field_error('name', _('Enter a valid SRV name')) + else: + cleaned_data['name'] += domain_name + else: + if not re.match(WILDCARD_DOMAIN_NAME_REGEX, + cleaned_data['name']): + self._add_field_error('name', _('Enter a valid hostname')) + elif not cleaned_data['name'].endswith(domain_name): + self._add_field_error( + 'name', + _('Name must be in the current domain')) + + # Data field + if self._is_field_blank(cleaned_data, 'data'): + if record_type in ['A', 'AAAA', 'CNAME', 'MX', 'SRV']: + self._add_required_field_error('data') + else: + if record_type == 'A': + try: + validators.validate_ipv4_address(cleaned_data['data']) + except ValidationError: + self._add_field_error('data', + _('Enter a valid IPv4 address')) + + elif record_type == 'AAAA': + try: + validators.validate_ipv6_address(cleaned_data['data']) + except ValidationError: + self._add_field_error('data', + _('Enter a valid IPv6 address')) + + elif record_type in ['CNAME', 'MX', 'PTR']: + if not re.match(DOMAIN_NAME_REGEX, cleaned_data['data']): + self._add_field_error('data', _('Enter a valid hostname')) + + elif record_type == 'SRV': + if not re.match(SRV_DATA_REGEX, cleaned_data['data']): + self._add_field_error('data', + _('Enter a valid SRV record')) + + # Txt field + if self._is_field_blank(cleaned_data, 'txt'): + if record_type == 'TXT': + self._add_required_field_error('txt') + else: + if record_type == 'TXT': + cleaned_data['data'] = cleaned_data['txt'] + + cleaned_data.pop('txt') + + # Priority field + if self._is_field_blank(cleaned_data, 'priority'): + if record_type in ['MX', 'SRV']: + self._add_required_field_error('priority') + + # Rename 'id' to 'record_id' + if 'id' in cleaned_data: + cleaned_data['record_id'] = cleaned_data.pop('id') + + # Remove domain_name + cleaned_data.pop('domain_name') + + return cleaned_data + + def _add_required_field_error(self, field): + '''Set a required field error on the specified field.''' + self._add_field_error(field, _('This field is required')) + + def _add_field_error(self, field, msg): + '''Set the specified msg as an error on the field.''' + self._errors[field] = self.error_class([msg]) + + def _is_field_blank(self, cleaned_data, field): + '''Returns a flag indicating whether the specified field is blank.''' + return field in cleaned_data and not cleaned_data[field] + + +class RecordCreate(RecordForm): + + '''Form for creating a new domain record.''' + + def handle(self, request, data): + try: + record = api.designate.record_create(request, **data) + messages.success(request, + _('Domain record %(name)s created.') % + {"name": record.name}) + return record + + except designate_exceptions.RemoteError as ex: + + if ex.code >= 500: + self.api_error( + _('Error: %(message)s (Request ID: %(request_id)s)') + % { + "request_id": ex.request_id, + "message": ex.message + }) + else: + self.api_error( + _('Error: %(message)s') + % { + "message": ex.message + }) + + return False + + except Exception: + messages.error(request, _('Unable to create record.')) + return True + + +class RecordUpdate(RecordForm): + + '''Form for editing a domain record.''' + + id = forms.CharField(widget=forms.HiddenInput()) + + def __init__(self, request, *args, **kwargs): + super(RecordUpdate, self).__init__(request, *args, **kwargs) + + # Force the type field to be read-only + self.fields['type'].widget.attrs['readonly'] = 'readonly' + + if self['type'].value() in ('soa', 'ns'): + self.fields['type'].choices.append(('ns', _('NS'))) + self.fields['type'].choices.append(('soa', _('SOA'))) + + self.fields['name'].widget.attrs['readonly'] = 'readonly' + self.fields['data'].widget.attrs['readonly'] = 'readonly' + self.fields['description'].widget.attrs['readonly'] = 'readonly' + self.fields['ttl'].widget.attrs['readonly'] = 'readonly' + + # Filter the choice list so that it only contains the type for + # the current record. Ideally, we would just disable the select + # field, but that has the unfortunate side-effect of breaking + # the 'selectable' javascript code. + self.fields['type'].choices = ( + [choice for choice in self.fields['type'].choices + if choice[0] == self.initial['type']]) + + def handle(self, request, data): + + if data['type'] in ('SOA', 'NS'): + return True + + try: + record = api.designate.record_update(request, **data) + + messages.success(request, + _('Domain record %(name)s updated.') % + {"name": record.name}) + + return record + + except designate_exceptions.RemoteError as ex: + if ex.code >= 500: + self.api_error( + _('Error: %(message)s (Request ID: %(request_id)s)') + % { + "request_id": ex.request_id, + "message": ex.message + }) + else: + self.api_error( + _('Error: %(message)s') + % { + "message": ex.message + }) + + return False + + except Exception: + messages.error(request, _('Unable to create record.')) + return True diff --git a/designatedashboard/dashboards/project/dns_domains/panel.py b/designatedashboard/dashboards/project/dns_domains/panel.py new file mode 100644 index 0000000..a7df87f --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/panel.py @@ -0,0 +1,26 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 _ # noqa + +import horizon +from openstack_dashboard.dashboards.project import dashboard + + +class DNSDomains(horizon.Panel): + name = _("Domains") + slug = 'dns_domains' + permissions = ('openstack.services.dns',) + + +dashboard.Project.register(DNSDomains) diff --git a/designatedashboard/dashboards/project/dns_domains/tables.py b/designatedashboard/dashboards/project/dns_domains/tables.py new file mode 100644 index 0000000..2eec2ec --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/tables.py @@ -0,0 +1,193 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 _ # noqa + +from horizon import tables + +from designatedashboard import api + +LOG = logging.getLogger(__name__) + +EDITABLE_RECORD_TYPES = ( + "A", + "AAAA", + "CNAME", + "MX", + "PTR", + "SPF", + "SRV", + "SSHFP", + "TXT", +) + + +class CreateDomain(tables.LinkAction): + + '''Link action for navigating to the CreateDomain view.''' + name = "create_domain" + verbose_name = _("Create Domain") + url = "horizon:project:dns_domains:create_domain" + classes = ("ajax-modal", "btn-create") + + +class EditDomain(tables.LinkAction): + + '''Link action for navigating to the UpdateDomain view.''' + name = "edit_domain" + verbose_name = _("Edit Domain") + url = "horizon:project:dns_domains:update_domain" + classes = ("ajax-modal", "btn-edit") + + +class ManageRecords(tables.LinkAction): + + '''Link action for navigating to the ManageRecords view.''' + name = "manage_records" + verbose_name = _("Manage Records") + url = "horizon:project:dns_domains:records" + classes = ("btn-edit") + + +class DeleteDomain(tables.BatchAction): + + '''Batch action for deleting domains.''' + name = "delete" + action_present = _("Delete") + action_past = _("Deleted") + data_type_singular = _("Domain") + data_type_plural = _("Domains") + classes = ('btn-danger', 'btn-delete') + + def action(self, request, domain_id): + api.designate.domain_delete(request, domain_id) + + +class CreateRecord(tables.LinkAction): + + '''Link action for navigating to the CreateRecord view.''' + name = "create_record" + verbose_name = _("Create Record") + classes = ("ajax-modal", "btn-create") + + def get_link_url(self, datum=None): + url = "horizon:project:dns_domains:create_record" + return urlresolvers.reverse(url, kwargs=self.table.kwargs) + + +class EditRecord(tables.LinkAction): + + '''Link action for navigating to the UpdateRecord view.''' + name = "edit_record" + verbose_name = _("Edit Record") + classes = ("ajax-modal", "btn-edit") + + def get_link_url(self, datum=None): + url = "horizon:project:dns_domains:update_record" + kwargs = { + 'domain_id': datum.domain_id, + 'record_id': datum.id, + } + + return urlresolvers.reverse(url, kwargs=kwargs) + + def allowed(self, request, record=None): + return record.type in EDITABLE_RECORD_TYPES + + +class DeleteRecord(tables.BatchAction): + + '''Batch action for deleting domain records.''' + + name = "delete" + action_present = _("Delete") + action_past = _("Deleted") + data_type_singular = _("Record") + data_type_plural = _("Records") + classes = ('btn-danger', 'btn-delete') + + def action(self, request, record_id): + domain_id = self.table.kwargs['domain_id'] + api.designate.record_delete(request, domain_id, record_id) + + def allowed(self, request, record=None): + return record.type in EDITABLE_RECORD_TYPES + + +class DomainsTable(tables.DataTable): + + '''Data table for displaying domain summary information.''' + + name = tables.Column("name", + verbose_name=_("Name"), + link=("horizon:project:dns_domains:update_domain"), + link_classes=('ajax-modal',)) + + email = tables.Column("email", + verbose_name=_("Email")) + + ttl = tables.Column("ttl", + verbose_name=_("TTL")) + + serial = tables.Column("serial", + verbose_name=_("Serial")) + + class Meta: + name = "domains" + verbose_name = _("Domains") + table_actions = (CreateDomain, DeleteDomain,) + row_actions = (ManageRecords, EditDomain, DeleteDomain,) + + +def update_record_link(record): + '''Returns a link to the view for updating DNS records.''' + + return urlresolvers.reverse( + "horizon:project:dns_domains:update_record", + args=(record.domain_id, record.id)) + + +class RecordsTable(tables.DataTable): + + '''Data table for displaying summary information for a domains records.''' + + name = tables.Column("name", + verbose_name=_("Name"), + link=update_record_link, + link_classes=('ajax-modal',) + ) + + type = tables.Column("type", + verbose_name=_("Type") + ) + + data = tables.Column("data", + verbose_name=_("Data") + ) + + priority = tables.Column("priority", + verbose_name=_("Priority"), + ) + + ttl = tables.Column("ttl", + verbose_name=_("TTL") + ) + + class Meta: + name = "records" + verbose_name = _("Records") + table_actions = (CreateRecord, DeleteRecord,) + row_actions = (EditRecord, DeleteRecord,) diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_create_domain.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_create_domain.html new file mode 100644 index 0000000..4205741 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_create_domain.html @@ -0,0 +1,38 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url 'horizon:project:dns_domains:create_domain' %}{% endblock %} + +{% block modal_id %}create_domain_modal{% endblock %} +{% block modal-header %}{% trans "Create Domain" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+ +
+

{% trans "Description" %}:

+

{% blocktrans %} + The Name field should contain a full-qualified domain name (with + trailing period). + {% endblocktrans %}

+

{% blocktrans %} + The Email field should contain a valid email address to be associated + with the domain. + {% endblocktrans %}

+

{% blocktrans %} + The optional TTL field can be any value between 0 and 2147483647 + seconds. + {% endblocktrans %}

+
+ +{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_create_record.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_create_record.html new file mode 100644 index 0000000..ef9bdc3 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_create_record.html @@ -0,0 +1,21 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n horizon humanize %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url 'horizon:project:dns_domains:create_record' domain.id %}{% endblock %} + +{% block modal_id %}create_record_modal{% endblock %} +{% block modal-header %}{% trans "Create Record for" %} {{ domain.name }}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_update_domain.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_update_domain.html new file mode 100644 index 0000000..b6b4e1f --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_update_domain.html @@ -0,0 +1,36 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}update_domain_form{% endblock %} +{% block form_action %}{% url 'horizon:project:dns_domains:update_domain' domain.id %}{% endblock %} + +{% block modal-header %}{% trans "Update Domain" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+ +
+

{% trans "Description" %}:

+

{% blocktrans %} + From here you can edit the email address and TTL associated with a domain. + {% endblocktrans %}

+

{% blocktrans %} + The Email field should contain a valid email address to be associated + with the domain. + {% endblocktrans %}

+

{% blocktrans %} + The optional TTL field can be any value between 0 and 2147483647 + seconds. + {% endblocktrans %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_update_record.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_update_record.html new file mode 100644 index 0000000..65b8fda --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/_update_record.html @@ -0,0 +1,21 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}update_record_form{% endblock %} +{% block form_action %}{% url 'horizon:project:dns_domains:update_record' record.domain_id record.id %}{% endblock %} + +{% block modal-header %}{% trans "Update Domain Record" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/create_domain.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/create_domain.html new file mode 100644 index 0000000..fe6380c --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/create_domain.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Domain" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Domain") %} +{% endblock page_header %} + +{% block main %} + {% include 'project/dns_domains/_create_domain.html' %} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/create_record.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/create_record.html new file mode 100644 index 0000000..4414f32 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/create_record.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Domain Record" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Domain Record") %} +{% endblock page_header %} + +{% block main %} + {% include 'project/dns_domains/_create_record.html' %} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/index.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/index.html new file mode 100644 index 0000000..a7d629b --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Domains" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Domains") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/records.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/records.html new file mode 100644 index 0000000..b478180 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/records.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans 'Domain Records' %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Domain Records") %} +{% endblock page_header %} + +{% block main %} +
+
+
+

+ {% trans "Domains" %} : {{ domain.name }} → + {% trans "Records" %} +

+
+
+ × +
+
+
+

Nameservers

+
    + {% for server in servers %} +
  • {{ server.name }}
  • + {% endfor %} +
+
+ + {{ table.render }} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/update_domain.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/update_domain.html new file mode 100644 index 0000000..fee32f2 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/update_domain.html @@ -0,0 +1,11 @@ +{% extends 'adminui/base.html' %} +{% load i18n %} +{% block title %}{% trans 'Update Domain' %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title="Domain" %} +{% endblock page_header %} + +{% block main %} + {% include 'project/dns_domains/_update_domain.html' %} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/update_record.html b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/update_record.html new file mode 100644 index 0000000..ff06057 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/templates/dns_domains/update_record.html @@ -0,0 +1,11 @@ +{% extends 'adminui/base.html' %} +{% load i18n %} +{% block title %}{% trans 'Update Domain Record' %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title="Domain Record" %} +{% endblock page_header %} + +{% block main %} + {% include 'project/dns_domains/_update_record.html' %} +{% endblock %} diff --git a/designatedashboard/dashboards/project/dns_domains/tests.py b/designatedashboard/dashboards/project/dns_domains/tests.py new file mode 100644 index 0000000..9466c52 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/tests.py @@ -0,0 +1,447 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# 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 __future__ import unicode_literals + +from django.core.urlresolvers import reverse # noqa +from django import http + +from mox import IsA # noqa + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + +from designatedashboard.dashboards.project.dns_domains import forms + + +DOMAIN_ID = '123' +INDEX_URL = reverse('horizon:project:dns_domains:index') +RECORDS_URL = reverse('horizon:project:dns_domains:records', args=[DOMAIN_ID]) + + +class DNSDomainsTests(test.TestCase): + + def setUp(self): + super(DNSDomainsTests, self).setUp() + + @test.create_stubs( + {api.designate: ('domain_list',)}) + def test_index(self): + domains = self.dns_domains.list() + api.designate.domain_list( + IsA(http.HttpRequest)).AndReturn(domains) + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + + self.assertTemplateUsed(res, 'project/dns_domains/index.html') + self.assertEqual(len(res.context['table'].data), len(domains)) + + @test.create_stubs( + {api.designate: ('domain_get', 'server_list', 'record_list')}) + def test_records(self): + domain_id = '123' + domain = self.dns_domains.first() + servers = self.dns_servers.list() + records = self.dns_records.list() + + api.designate.domain_get( + IsA(http.HttpRequest), + domain_id).AndReturn(domain) + + api.designate.server_list( + IsA(http.HttpRequest), + domain_id).AndReturn(servers) + + api.designate.record_list( + IsA(http.HttpRequest), + domain_id).AndReturn(records) + + self.mox.ReplayAll() + + res = self.client.get(RECORDS_URL) + + self.assertTemplateUsed(res, 'project/dns_domains/records.html') + self.assertEqual(len(res.context['table'].data), len(records)) + + +class BaseRecordFormCleanTests(test.TestCase): + + DOMAIN_NAME = 'foo.com.' + HOSTNAME = 'www.foo.com.' + + MSG_FIELD_REQUIRED = 'This field is required' + MSG_INVALID_HOSTNAME = 'Enter a valid hostname' + MSG_OUTSIDE_DOMAIN = 'Name must be in the current domain' + + def setUp(self): + super(BaseRecordFormCleanTests, self).setUp() + + # Request object with messages support + self.request = self.factory.get('', {}) + + # Set-up form instance + self.form = forms.RecordCreate(self.request) + self.form._errors = {} + self.form.cleaned_data = { + 'domain_name': self.DOMAIN_NAME, + 'name': '', + 'data': '', + 'txt': '', + 'priority': None, + 'ttl': None, + } + + def assert_no_errors(self): + self.assertEqual(self.form._errors, {}) + + def assert_error(self, field, msg): + self.assertIn(msg, self.form._errors[field]) + + def assert_required_error(self, field): + self.assert_error(field, self.MSG_FIELD_REQUIRED) + + +class ARecordFormTests(BaseRecordFormCleanTests): + + IPV4 = '1.1.1.1' + + MSG_INVALID_IPV4 = 'Enter a valid IPv4 address' + + def setUp(self): + super(ARecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'A' + self.form.cleaned_data['name'] = self.HOSTNAME + self.form.cleaned_data['data'] = self.IPV4 + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_valid_name_field_wild_card(self): + self.form.cleaned_data['name'] = '*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_starting_dash(self): + self.form.cleaned_data['name'] = '-ww.foo.com' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_trailing_dash(self): + self.form.cleaned_data['name'] = 'www.foo.co-' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_bad_wild_card(self): + self.form.cleaned_data['name'] = 'derp.*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_outside_of_domain_name_field(self): + self.form.cleaned_data['name'] = 'www.bar.com.' + self.form.clean() + self.assert_error('name', self.MSG_OUTSIDE_DOMAIN) + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_IPV4) + + +class AAAARecordFormTests(BaseRecordFormCleanTests): + + IPV6 = '1111:1111:1111:11::1' + + MSG_INVALID_IPV6 = 'Enter a valid IPv6 address' + + def setUp(self): + super(AAAARecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'AAAA' + self.form.cleaned_data['name'] = self.HOSTNAME + self.form.cleaned_data['data'] = self.IPV6 + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_valid_name_field_wild_card(self): + self.form.cleaned_data['name'] = '*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_starting_dash(self): + self.form.cleaned_data['name'] = '-ww.foo.com' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_trailing_dash(self): + self.form.cleaned_data['name'] = 'www.foo.co-' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_bad_wild_card(self): + self.form.cleaned_data['name'] = 'derp.*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_outside_of_domain_name_field(self): + self.form.cleaned_data['name'] = 'www.bar.com.' + self.form.clean() + self.assert_error('name', self.MSG_OUTSIDE_DOMAIN) + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_IPV6) + + +class CNAMERecordFormTests(BaseRecordFormCleanTests): + + CNAME = 'bar.foo.com.' + + def setUp(self): + super(CNAMERecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'CNAME' + self.form.cleaned_data['name'] = self.HOSTNAME + self.form.cleaned_data['data'] = self.CNAME + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_valid_name_field_wild_card(self): + self.form.cleaned_data['name'] = '*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_starting_dash(self): + self.form.cleaned_data['name'] = '-ww.foo.com' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_trailing_dash(self): + self.form.cleaned_data['name'] = 'www.foo.co-' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_bad_wild_card(self): + self.form.cleaned_data['name'] = 'derp.*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_outside_of_domain_name_field(self): + self.form.cleaned_data['name'] = 'www.bar.com.' + self.form.clean() + self.assert_error('name', self.MSG_OUTSIDE_DOMAIN) + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_HOSTNAME) + + +class MXRecordFormTests(BaseRecordFormCleanTests): + + MAIL_SERVER = 'mail.foo.com.' + PRIORITY = 10 + + def setUp(self): + super(MXRecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'MX' + self.form.cleaned_data['data'] = self.MAIL_SERVER + self.form.cleaned_data['priority'] = self.PRIORITY + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_missing_priority_field(self): + self.form.cleaned_data['priority'] = None + self.form.clean() + self.assert_required_error('priority') + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_HOSTNAME) + + def test_default_assignment_name_field(self): + self.form.clean() + self.assertEqual(self.DOMAIN_NAME, self.form.cleaned_data['name']) + + +class TXTRecordFormTests(BaseRecordFormCleanTests): + + TEXT = 'Lorem ipsum' + + def setUp(self): + super(TXTRecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'TXT' + self.form.cleaned_data['name'] = self.HOSTNAME + self.form.cleaned_data['txt'] = self.TEXT + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_valid_name_field_wild_card(self): + self.form.cleaned_data['name'] = '*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_txt_field(self): + self.form.cleaned_data['txt'] = '' + self.form.clean() + self.assert_required_error('txt') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_starting_dash(self): + self.form.cleaned_data['name'] = '-ww.foo.com' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_trailing_dash(self): + self.form.cleaned_data['name'] = 'www.foo.co-' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_bad_wild_card(self): + self.form.cleaned_data['name'] = 'derp.*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_outside_of_domain_name_field(self): + self.form.cleaned_data['name'] = 'www.bar.com.' + self.form.clean() + self.assert_error('name', self.MSG_OUTSIDE_DOMAIN) + + def test_default_assignment_data_field(self): + self.form.clean() + self.assertEqual(self.TEXT, self.form.cleaned_data['data']) + + +class SRVRecordFormTests(BaseRecordFormCleanTests): + + SRV_NAME = '_foo._tcp.' + SRV_DATA = '1 1 srv.foo.com.' + PRIORITY = 10 + + MSG_INVALID_SRV_NAME = 'Enter a valid SRV name' + MSG_INVALID_SRV_DATA = 'Enter a valid SRV record' + + def setUp(self): + super(SRVRecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'SRV' + self.form.cleaned_data['name'] = self.SRV_NAME + self.form.cleaned_data['data'] = self.SRV_DATA + self.form.cleaned_data['priority'] = self.PRIORITY + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_missing_priority_field(self): + self.form.cleaned_data['priority'] = None + self.form.clean() + self.assert_required_error('priority') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_SRV_NAME) + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_SRV_DATA) + + def test_default_assignment_name_field(self): + self.form.clean() + self.assertEqual(self.SRV_NAME + self.DOMAIN_NAME, + self.form.cleaned_data['name']) diff --git a/designatedashboard/dashboards/project/dns_domains/urls.py b/designatedashboard/dashboards/project/dns_domains/urls.py new file mode 100644 index 0000000..96450b9 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/urls.py @@ -0,0 +1,44 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 # noqa + +from .views import CreateDomainView # noqa +from .views import CreateRecordView # noqa +from .views import IndexView # noqa +from .views import RecordsView # noqa +from .views import UpdateDomainView # noqa +from .views import UpdateRecordView # noqa + + +urlpatterns = patterns( + '', + url(r'^$', + IndexView.as_view(), + name='index'), + url(r'^create/$', + CreateDomainView.as_view(), + name='create_domain'), + url(r'^(?P[^/]+)/update$', + UpdateDomainView.as_view(), + name='update_domain'), + url(r'^(?P[^/]+)/records$', + RecordsView.as_view(), + name='records'), + url(r'^(?P[^/]+)/records/create$', + CreateRecordView.as_view(), + name='create_record'), + url(r'^(?P[^/]+)/records/(?P[^/]+)/$', + UpdateRecordView.as_view(), + name='update_record'), +) diff --git a/designatedashboard/dashboards/project/dns_domains/views.py b/designatedashboard/dashboards/project/dns_domains/views.py new file mode 100644 index 0000000..e9d5c27 --- /dev/null +++ b/designatedashboard/dashboards/project/dns_domains/views.py @@ -0,0 +1,181 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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.core.urlresolvers import reverse, reverse_lazy # noqa +from django.utils.translation import ugettext_lazy as _ # noqa + +from horizon import exceptions +from horizon import forms +from horizon import tables + +from designatedashboard import api + +from .forms import DomainCreate # noqa +from .forms import DomainUpdate # noqa +from .forms import RecordCreate # noqa +from .forms import RecordUpdate # noqa +from .tables import DomainsTable # noqa +from .tables import RecordsTable # noqa + + +class IndexView(tables.DataTableView): + table_class = DomainsTable + template_name = 'project/dns_domains/index.html' + + def get_data(self): + try: + return api.designate.domain_list(self.request) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve domain list.')) + return [] + + +class CreateDomainView(forms.ModalFormView): + form_class = DomainCreate + template_name = 'project/dns_domains/create_domain.html' + success_url = reverse_lazy('horizon:project:dns_domains:index') + + def get_object_display(self, obj): + return obj.ip + + +class UpdateDomainView(forms.ModalFormView): + form_class = DomainUpdate + template_name = 'project/dns_domains/update_domain.html' + success_url = reverse_lazy('horizon:project:dns_domains:index') + + def get_object(self): + domain_id = self.kwargs['domain_id'] + try: + return api.designate.domain_get(self.request, domain_id) + except Exception: + redirect = reverse('horizon:project:dns_domains:index') + exceptions.handle(self.request, + _('Unable to retrieve domain record.'), + redirect=redirect) + + def get_initial(self): + self.domain = self.get_object() + return self.domain + + def get_context_data(self, **kwargs): + context = super(UpdateDomainView, self).get_context_data(**kwargs) + context["domain"] = self.domain + return context + + +class RecordsView(tables.DataTableView): + table_class = RecordsTable + template_name = 'project/dns_domains/records.html' + + def get_data(self): + domain_id = self.kwargs['domain_id'] + try: + self.domain = api.designate.domain_get(self.request, domain_id) + self.servers = api.designate.server_list(self.request, domain_id) + records = api.designate.record_list(self.request, domain_id) + except Exception: + redirect = reverse('horizon:project:dns_domains:index') + exceptions.handle(self.request, + _('Unable to retrieve record list.'), + redirect=redirect) + + # TODO(Matt): This may not be defined here. + return records + + def get_context_data(self, **kwargs): + context = super(RecordsView, self).get_context_data(**kwargs) + context['domain'] = self.domain + context['servers'] = self.servers + + return context + + +class BaseRecordFormView(forms.ModalFormView): + + def get_success_url(self): + return reverse('horizon:project:dns_domains:records', + args=(self.kwargs['domain_id'],)) + + def get_domain(self): + domain_id = self.kwargs['domain_id'] + try: + return api.designate.domain_get(self.request, domain_id) + except Exception: + redirect = reverse('horizon:project:dns_domains:records', + args=(self.kwargs['domain_id'],)) + exceptions.handle(self.request, + ('Unable to retrieve domain record.'), + redirect=redirect) + # NotAuthorized errors won't be redirected automatically. Need + # to force the issue + raise exceptions.Http302(redirect) + + def get_initial(self): + self.domain = self.get_domain() + + return { + 'domain_id': self.domain.id, + 'domain_name': self.domain.name, + } + + def get_context_data(self, **kwargs): + context = super(BaseRecordFormView, self).get_context_data(**kwargs) + context['domain'] = self.domain + return context + + +class CreateRecordView(BaseRecordFormView): + form_class = RecordCreate + template_name = 'project/dns_domains/create_record.html' + + +class UpdateRecordView(BaseRecordFormView): + form_class = RecordUpdate + template_name = 'project/dns_domains/update_record.html' + + def get_record(self): + domain_id = self.kwargs['domain_id'] + record_id = self.kwargs['record_id'] + + try: + return api.designate.record_get(self.request, domain_id, record_id) + except Exception: + redirect = reverse('horizon:project:dns_domains:records', + args=(self.kwargs['domain_id'],)) + exceptions.handle(self.request, + _('Unable to retrieve domain record.'), + redirect=redirect) + + def get_initial(self): + initial = super(UpdateRecordView, self).get_initial() + self.record = self.get_record() + + initial.update({ + 'id': self.record.id, + 'name': self.record.name, + 'data': self.record.data, + 'txt': self.record.data, + 'priority': self.record.priority, + 'ttl': self.record.ttl, + 'type': self.record.type.lower(), + 'description': self.record.description, + }) + + return initial + + def get_context_data(self, **kwargs): + context = super(UpdateRecordView, self).get_context_data(**kwargs) + context["record"] = self.record + return context diff --git a/designatedashboard/exceptions.py b/designatedashboard/exceptions.py new file mode 100644 index 0000000..20cd1e6 --- /dev/null +++ b/designatedashboard/exceptions.py @@ -0,0 +1,21 @@ +# Copyright (c) 2014 Rackspace Hosting. +# +# 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 openstack_dashboard import exceptions + +NOT_FOUND = exceptions.NOT_FOUND +RECOVERABLE = exceptions.RECOVERABLE +# + (solumclient.ClientException,) +UNAUTHORIZED = exceptions.UNAUTHORIZED diff --git a/designatedashboard/tests/.secret_key_store b/designatedashboard/tests/.secret_key_store new file mode 100644 index 0000000..6e8db9b --- /dev/null +++ b/designatedashboard/tests/.secret_key_store @@ -0,0 +1 @@ +5NMb6ZfXYBLClFGFYf6VkbiJ9TRNyU3w8NQHPG8LXJltU8EZFeB7I632vQ8MF5m6 \ No newline at end of file diff --git a/designatedashboard/tests/__init__.py b/designatedashboard/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designatedashboard/tests/base.py b/designatedashboard/tests/base.py new file mode 100644 index 0000000..3bf12a8 --- /dev/null +++ b/designatedashboard/tests/base.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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 os + +import fixtures +import testtools + +_TRUE_VALUES = ('True', 'true', '1', 'yes') + + +class TestCase(testtools.TestCase): + + """Test case base class for all unit tests.""" + + def setUp(self): + """Run before each test method to initialize test environment.""" + + super(TestCase, self).setUp() + test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) + try: + test_timeout = int(test_timeout) + except ValueError: + # If timeout value is invalid do not set a timeout. + test_timeout = 0 + if test_timeout > 0: + self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) + + self.useFixture(fixtures.NestedTempfile()) + self.useFixture(fixtures.TempHomeDir()) + + if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + + self.log_fixture = self.useFixture(fixtures.FakeLogger()) \ No newline at end of file diff --git a/designatedashboard/tests/settings.py b/designatedashboard/tests/settings.py new file mode 100644 index 0000000..57f8d13 --- /dev/null +++ b/designatedashboard/tests/settings.py @@ -0,0 +1,133 @@ +# Copyright (c) 2014 Rackspace Hosting. +# +# 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 os + +from django.utils.translation import ugettext as _ + +from horizon.test.settings import * # noqa +from horizon.utils import secret_key as secret_key_utils + +from designatedashboard import exceptions + + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, "..")) + +SECRET_KEY = secret_key_utils.generate_or_read_from_file( + os.path.join(TEST_DIR, '.secret_key_store')) +ROOT_URLCONF = 'openstack_dashboard.urls' +TEMPLATE_DIRS = ( + os.path.join(TEST_DIR, 'templates'), +) + +TEMPLATE_CONTEXT_PROCESSORS += ( + 'openstack_dashboard.context_processors.openstack', +) + +INSTALLED_APPS = ( + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.staticfiles', + 'django.contrib.messages', + 'django.contrib.humanize', + 'django_nose', + 'openstack_auth', + 'compressor', + 'horizon', + 'openstack_dashboard', + 'openstack_dashboard.dashboards.project', + 'openstack_dashboard.dashboards.admin', + 'designatedashboard.dashboards.project.dns_domains', + 'openstack_dashboard.dashboards.settings', +) + +AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',) + +SITE_BRANDING = 'OpenStack' + +HORIZON_CONFIG = { + 'dashboards': ('project', 'admin', 'infrastructure', 'settings'), + 'default_dashboard': 'project', + "password_validator": { + "regex": '^.{8,18}$', + "help_text": _("Password must be between 8 and 18 characters.") + }, + 'user_home': None, + 'help_url': "http://docs.openstack.org", + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, +} + +# Set to True to allow users to upload images to glance via Horizon server. +# When enabled, a file form field will appear on the create image form. +# See documentation for deployment considerations. +HORIZON_IMAGES_ALLOW_UPLOAD = True + +OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0" +OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" + +OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True +OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'test_domain' + +OPENSTACK_KEYSTONE_BACKEND = { + 'name': 'native', + 'can_edit_user': True, + 'can_edit_group': True, + 'can_edit_project': True, + 'can_edit_domain': True, + 'can_edit_role': True +} + +OPENSTACK_HYPERVISOR_FEATURES = { + 'can_set_mount_point': True, + + # NOTE: as of Grizzly this is not yet supported in Nova so enabling this + # setting will not do anything useful + 'can_encrypt_volumes': False +} + +LOGGING['loggers']['openstack_dashboard'] = { + 'handlers': ['test'], + 'propagate': False, +} + +SECURITY_GROUP_RULES = { + 'all_tcp': { + 'name': 'ALL TCP', + 'ip_protocol': 'tcp', + 'from_port': '1', + 'to_port': '65535', + }, + 'http': { + 'name': 'HTTP', + 'ip_protocol': 'tcp', + 'from_port': '80', + 'to_port': '80', + }, +} + +NOSE_ARGS = ['--nocapture', + '--nologcapture', + '--cover-package=openstack_dashboard', + '--cover-inclusive', + '--all-modules'] + +DESIGNATE_ENDPOINT_URL = "http://127.0.0.1:8000" + +DATABASES = ( + {'default': {'NAME': 'test', 'ENGINE': 'django.db.backends.sqlite3'}}) diff --git a/designatedashboard/tests/test_designatedashboard.py b/designatedashboard/tests/test_designatedashboard.py new file mode 100644 index 0000000..5dbed17 --- /dev/null +++ b/designatedashboard/tests/test_designatedashboard.py @@ -0,0 +1,448 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# 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 __future__ import unicode_literals + +from django.core.urlresolvers import reverse # noqa +from django import http + +from mox import IsA # noqa + +from designatedashboard import api + +from openstack_dashboard.test import helpers as test + +from designatedashboard.dashboards.project.dns_domains import forms + + +DOMAIN_ID = '123' +INDEX_URL = reverse('horizon:project:dns_domains:index') +RECORDS_URL = reverse('horizon:project:dns_domains:records', args=[DOMAIN_ID]) + + +class DNSDomainsTests(test.TestCase): + + def setUp(self): + super(DNSDomainsTests, self).setUp() + + @test.create_stubs( + {api.designate: ('domain_list',)}) + def test_index(self): + domains = self.dns_domains.list() + api.designate.domain_list( + IsA(http.HttpRequest)).AndReturn(domains) + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + + self.assertTemplateUsed(res, 'project/dns_domains/index.html') + self.assertEqual(len(res.context['table'].data), len(domains)) + + @test.create_stubs( + {api.designate: ('domain_get', 'server_list', 'record_list')}) + def test_records(self): + domain_id = '123' + domain = self.dns_domains.first() + servers = self.dns_servers.list() + records = self.dns_records.list() + + api.designate.domain_get( + IsA(http.HttpRequest), + domain_id).AndReturn(domain) + + api.designate.server_list( + IsA(http.HttpRequest), + domain_id).AndReturn(servers) + + api.designate.record_list( + IsA(http.HttpRequest), + domain_id).AndReturn(records) + + self.mox.ReplayAll() + + res = self.client.get(RECORDS_URL) + + self.assertTemplateUsed(res, 'project/dns_domains/records.html') + self.assertEqual(len(res.context['table'].data), len(records)) + + +class BaseRecordFormCleanTests(test.TestCase): + + DOMAIN_NAME = 'foo.com.' + HOSTNAME = 'www.foo.com.' + + MSG_FIELD_REQUIRED = 'This field is required' + MSG_INVALID_HOSTNAME = 'Enter a valid hostname' + MSG_OUTSIDE_DOMAIN = 'Name must be in the current domain' + + def setUp(self): + super(BaseRecordFormCleanTests, self).setUp() + + # Request object with messages support + self.request = self.factory.get('', {}) + + # Set-up form instance + self.form = forms.RecordCreate(self.request) + self.form._errors = {} + self.form.cleaned_data = { + 'domain_name': self.DOMAIN_NAME, + 'name': '', + 'data': '', + 'txt': '', + 'priority': None, + 'ttl': None, + } + + def assert_no_errors(self): + self.assertEqual(self.form._errors, {}) + + def assert_error(self, field, msg): + self.assertIn(msg, self.form._errors[field]) + + def assert_required_error(self, field): + self.assert_error(field, self.MSG_FIELD_REQUIRED) + + +class ARecordFormTests(BaseRecordFormCleanTests): + + IPV4 = '1.1.1.1' + + MSG_INVALID_IPV4 = 'Enter a valid IPv4 address' + + def setUp(self): + super(ARecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'A' + self.form.cleaned_data['name'] = self.HOSTNAME + self.form.cleaned_data['data'] = self.IPV4 + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_valid_name_field_wild_card(self): + self.form.cleaned_data['name'] = '*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_starting_dash(self): + self.form.cleaned_data['name'] = '-ww.foo.com' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_trailing_dash(self): + self.form.cleaned_data['name'] = 'www.foo.co-' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_bad_wild_card(self): + self.form.cleaned_data['name'] = 'derp.*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_outside_of_domain_name_field(self): + self.form.cleaned_data['name'] = 'www.bar.com.' + self.form.clean() + self.assert_error('name', self.MSG_OUTSIDE_DOMAIN) + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_IPV4) + + +class AAAARecordFormTests(BaseRecordFormCleanTests): + + IPV6 = '1111:1111:1111:11::1' + + MSG_INVALID_IPV6 = 'Enter a valid IPv6 address' + + def setUp(self): + super(AAAARecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'AAAA' + self.form.cleaned_data['name'] = self.HOSTNAME + self.form.cleaned_data['data'] = self.IPV6 + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_valid_name_field_wild_card(self): + self.form.cleaned_data['name'] = '*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_starting_dash(self): + self.form.cleaned_data['name'] = '-ww.foo.com' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_trailing_dash(self): + self.form.cleaned_data['name'] = 'www.foo.co-' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_bad_wild_card(self): + self.form.cleaned_data['name'] = 'derp.*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_outside_of_domain_name_field(self): + self.form.cleaned_data['name'] = 'www.bar.com.' + self.form.clean() + self.assert_error('name', self.MSG_OUTSIDE_DOMAIN) + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_IPV6) + + +class CNAMERecordFormTests(BaseRecordFormCleanTests): + + CNAME = 'bar.foo.com.' + + def setUp(self): + super(CNAMERecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'CNAME' + self.form.cleaned_data['name'] = self.HOSTNAME + self.form.cleaned_data['data'] = self.CNAME + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_valid_name_field_wild_card(self): + self.form.cleaned_data['name'] = '*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_starting_dash(self): + self.form.cleaned_data['name'] = '-ww.foo.com' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_trailing_dash(self): + self.form.cleaned_data['name'] = 'www.foo.co-' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_bad_wild_card(self): + self.form.cleaned_data['name'] = 'derp.*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_outside_of_domain_name_field(self): + self.form.cleaned_data['name'] = 'www.bar.com.' + self.form.clean() + self.assert_error('name', self.MSG_OUTSIDE_DOMAIN) + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_HOSTNAME) + + +class MXRecordFormTests(BaseRecordFormCleanTests): + + MAIL_SERVER = 'mail.foo.com.' + PRIORITY = 10 + + def setUp(self): + super(MXRecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'MX' + self.form.cleaned_data['data'] = self.MAIL_SERVER + self.form.cleaned_data['priority'] = self.PRIORITY + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_missing_priority_field(self): + self.form.cleaned_data['priority'] = None + self.form.clean() + self.assert_required_error('priority') + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_HOSTNAME) + + def test_default_assignment_name_field(self): + self.form.clean() + self.assertEqual(self.DOMAIN_NAME, self.form.cleaned_data['name']) + + +class TXTRecordFormTests(BaseRecordFormCleanTests): + + TEXT = 'Lorem ipsum' + + def setUp(self): + super(TXTRecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'TXT' + self.form.cleaned_data['name'] = self.HOSTNAME + self.form.cleaned_data['txt'] = self.TEXT + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_valid_name_field_wild_card(self): + self.form.cleaned_data['name'] = '*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_txt_field(self): + self.form.cleaned_data['txt'] = '' + self.form.clean() + self.assert_required_error('txt') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_starting_dash(self): + self.form.cleaned_data['name'] = '-ww.foo.com' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_trailing_dash(self): + self.form.cleaned_data['name'] = 'www.foo.co-' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_invalid_name_field_bad_wild_card(self): + self.form.cleaned_data['name'] = 'derp.*.' + self.DOMAIN_NAME + self.form.clean() + self.assert_error('name', self.MSG_INVALID_HOSTNAME) + + def test_outside_of_domain_name_field(self): + self.form.cleaned_data['name'] = 'www.bar.com.' + self.form.clean() + self.assert_error('name', self.MSG_OUTSIDE_DOMAIN) + + def test_default_assignment_data_field(self): + self.form.clean() + self.assertEqual(self.TEXT, self.form.cleaned_data['data']) + + +class SRVRecordFormTests(BaseRecordFormCleanTests): + + SRV_NAME = '_foo._tcp.' + SRV_DATA = '1 1 srv.foo.com.' + PRIORITY = 10 + + MSG_INVALID_SRV_NAME = 'Enter a valid SRV name' + MSG_INVALID_SRV_DATA = 'Enter a valid SRV record' + + def setUp(self): + super(SRVRecordFormTests, self).setUp() + self.form.cleaned_data['type'] = 'SRV' + self.form.cleaned_data['name'] = self.SRV_NAME + self.form.cleaned_data['data'] = self.SRV_DATA + self.form.cleaned_data['priority'] = self.PRIORITY + + def test_valid_field_values(self): + self.form.clean() + self.assert_no_errors() + + def test_missing_name_field(self): + self.form.cleaned_data['name'] = '' + self.form.clean() + self.assert_required_error('name') + + def test_missing_data_field(self): + self.form.cleaned_data['data'] = '' + self.form.clean() + self.assert_required_error('data') + + def test_missing_priority_field(self): + self.form.cleaned_data['priority'] = None + self.form.clean() + self.assert_required_error('priority') + + def test_invalid_name_field(self): + self.form.cleaned_data['name'] = 'foo' + self.form.clean() + self.assert_error('name', self.MSG_INVALID_SRV_NAME) + + def test_invalid_data_field(self): + self.form.cleaned_data['data'] = 'foo' + self.form.clean() + self.assert_error('data', self.MSG_INVALID_SRV_DATA) + + def test_default_assignment_name_field(self): + self.form.clean() + self.assertEqual(self.SRV_NAME + self.DOMAIN_NAME, + self.form.cleaned_data['name']) diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100755 index 0000000..c7ddc95 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# 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 reqdashboardred 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 os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a ndashboardsance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'designate_dashboard' +copyright = u'2013, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help bdashboardlder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} \ No newline at end of file diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 0000000..ed77c12 --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..a747d66 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,24 @@ +.. designate_ui documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to designatedashboard's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..af26cf0 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install designate_dashboard + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv designate_dashboard + $ pip install designate_dashboard \ No newline at end of file diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 0000000..38ba804 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..8ca3de4 --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use designate_dashboard in a project:: + + import designate_dashboard \ No newline at end of file diff --git a/enabled/_70_dns_add_group.py b/enabled/_70_dns_add_group.py new file mode 100644 index 0000000..4473e40 --- /dev/null +++ b/enabled/_70_dns_add_group.py @@ -0,0 +1,20 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +# The name of the panel group to be added to HORIZON_CONFIG. Required. +PANEL_GROUP = 'dns' +# The display name of the PANEL_GROUP. Required. +PANEL_GROUP_NAME = 'DNS' +# The name of the dashboard the PANEL_GROUP associated with. Required. +PANEL_GROUP_DASHBOARD = 'project' diff --git a/enabled/_71_dns_project.py b/enabled/_71_dns_project.py new file mode 100644 index 0000000..46899d2 --- /dev/null +++ b/enabled/_71_dns_project.py @@ -0,0 +1,26 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +PANEL = 'domains' + +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'domains' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'project' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'dns' + +# Python panel class of the PANEL to be added. +ADD_PANEL = ( + 'designatedashboard.dashboards.project.dns_domains.panel.DNSDomains') diff --git a/openstack-common.conf b/openstack-common.conf new file mode 100644 index 0000000..1d8dcca --- /dev/null +++ b/openstack-common.conf @@ -0,0 +1,6 @@ +[DEFAULT] + +# The list of modules to copy from oslo-incubator.git + +# The base module to hold the copy of openstack.common +base=designatedashboard diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..edbf8e3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +pbr>=0.6,!=0.7,<1.0 +# Horizon Core Requirements +Django>=1.4,<1.7 +django_compressor>=1.3 +django_openstack_auth>=1.1.4 +Babel>=0.9.6 +python-designateclient diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..48d352d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,46 @@ +[metadata] +name = designatedashboard +summary = Designate Horizon UI bits +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 2.6 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + +[files] +packages = + designatedashboard + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = designatedashboard/locale +domain = designatedashboard + +[update_catalog] +domain = designatedashboard +output_dir = designatedashboard/locale +input_file = designatedashboard/locale/designatedashboard.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = designatedashboard/locale/designatedashboard.pot \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..70c2b3f --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/test b/test new file mode 100644 index 0000000000000000000000000000000000000000..dd690e30fb899c8f50d20c98f0e24369a7db770f GIT binary patch literal 2048 zcmWFz^vNtqRY=P(%1ta$FlJz3U}R))P*7lCV2}V}CMafv@>zg141mN40SN}(XH2{x lRY0B0+XyKil^+d((GVai1iXk+HL7Sd1V%$(Gz4&l0041E2rvKu literal 0 HcmV?d00001 diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..393983c --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,14 @@ +hacking>=0.9.2,<0.10 + +coverage>=3.6 +discover +mock>=1.0 +mox +oslo.config>=1.2.1 +pylint==0.25.2 +testrepository>=0.0.18 +testtools>=0.9.34 +unittest2 +django_nose + +-e git+https://github.com/openstack/horizon.git#egg=horizon diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..53aa727 --- /dev/null +++ b/tox.ini @@ -0,0 +1,34 @@ +[tox] +minversion = 1.6 +envlist = py26,py27,py33,pypy,pep8 +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = python setup.py testr --slowest --testr-args='{posargs}' + +[testenv:pep8] +commands = flake8 + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py testr --coverage --testr-args='{posargs}' + +[testenv:docs] +commands = python setup.py build_sphinx + +[flake8] +# H803 skipped on purpose per list discussion. +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125,H803 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build \ No newline at end of file