diff --git a/horizon_dashboard/.gitignore b/horizon_dashboard/.gitignore new file mode 100644 index 00000000..18883d12 --- /dev/null +++ b/horizon_dashboard/.gitignore @@ -0,0 +1,20 @@ +*.pyc +*.swp +.environment_version +.selenium_log +.coverage* +.noseids +.venv +coverage.xml +pep8.txt +pylint.txt +reports +demo_dashboard/local/local_settings.py +/static/ +docs/build/ +docs/source/sourcecode +build +dist + +.DS_Store +.secret_key_store diff --git a/horizon_dashboard/README.rst b/horizon_dashboard/README.rst new file mode 100644 index 00000000..90191d4e --- /dev/null +++ b/horizon_dashboard/README.rst @@ -0,0 +1,31 @@ +==================================== +Horizon Customization Demo Dashboard +==================================== + +This Django project demonstrates how the `Horizon`_ app can be used to +construct customized dashboards (for OpenStack or anything else). + +The ``horizon`` module is pulled down from GitHub during setup +(see setup instructions below) and added to the virtual environment. + +.. _Horizon: http://github.com/openstack/horizon + +Setup Instructions +================== + +The following should get you started:: + + $ git clone https://github.com/stackforge/python-mistralclient.git + $ cd python-mistralclient/horizon_dashboard + $ cp demo_dashboard/local/local_settings.py.example demo_dashboard/local/local_settings.py + +Edit the ``local_settings.py`` file as needed. Make sure you have changed +OPENSTACK_HOST to point to your keystone server and also check all endpoints +are accessible. You may want to change OPENSTACK_ENDPOINT_TYPE to "publicURL" +if some of your endpoints are inaccessible. + +When you're ready to run the development server:: + + $ ./run_tests.sh --runserver + + diff --git a/horizon_dashboard/demo_dashboard/__init__.py b/horizon_dashboard/demo_dashboard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon_dashboard/demo_dashboard/dashboards/__init__.py b/horizon_dashboard/demo_dashboard/dashboards/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/__init__.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/api.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/api.py new file mode 100644 index 00000000..486e6256 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/api.py @@ -0,0 +1,5 @@ +from mistralclient.api import client as mistral_client + + +def mistralclient(request): + return mistral_client.Client() diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/dashboard.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/dashboard.py new file mode 100644 index 00000000..e3698cfd --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/dashboard.py @@ -0,0 +1,22 @@ +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class Default(horizon.Panel): + name = _("Default") + slug = 'default' + urls = 'demo_dashboard.dashboards.mistral.workbooks.urls' + nav = False + + +class MistralDashboard(horizon.Dashboard): + name = _("Mistral") + slug = "mistral" + panels = ('default', 'workbooks', 'executions',) + default_panel = 'default' + roles = ('admin',) + + +horizon.register(MistralDashboard) +MistralDashboard.register(Default) diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/__init__.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/panel.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/panel.py new file mode 100644 index 00000000..bf6dfcac --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/panel.py @@ -0,0 +1,12 @@ +from django.utils.translation import ugettext_lazy as _ + +import horizon +from demo_dashboard.dashboards.mistral import dashboard + + +class Executions(horizon.Panel): + name = _("Executions") + slug = 'executions' + + +dashboard.MistralDashboard.register(Executions) diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/tables.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/tables.py new file mode 100644 index 00000000..68b77749 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/tables.py @@ -0,0 +1,14 @@ +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + + +class ExecutionsTable(tables.DataTable): + id = tables.Column("id", verbose_name=_("ID")) + wb_name = tables.Column("workbook_name", verbose_name=_("Workbook")) + state = tables.Column("state", verbose_name=_("State")) + + class Meta: + name = "executions" + verbose_name = _("Executions") + # row_actions = (ExecuteWorkflow,) diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/templates/executions/index.html b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/templates/executions/index.html new file mode 100644 index 00000000..cf0671b4 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/templates/executions/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Executions" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Workbooks") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/urls.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/urls.py new file mode 100644 index 00000000..fca7a361 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns # noqa +from django.conf.urls import url # noqa + +from demo_dashboard.dashboards.mistral.executions.views import IndexView + +urlpatterns = patterns( + '', + url(r'^$', IndexView.as_view(), name='index'), +) diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/views.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/views.py new file mode 100644 index 00000000..2ef979e0 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/executions/views.py @@ -0,0 +1,14 @@ +from horizon import tables + +from demo_dashboard.dashboards.mistral import api +from demo_dashboard.dashboards.mistral.executions.tables import ExecutionsTable + + +class IndexView(tables.DataTableView): + table_class = ExecutionsTable + template_name = 'mistral/executions/index.html' + + def get_data(self): + client = api.mistralclient(self.request) + return [item for wb in client.workbooks.list() + for item in client.executions.list(wb.name)] diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/__init__.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/forms.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/forms.py new file mode 100644 index 00000000..38846027 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/forms.py @@ -0,0 +1,37 @@ +from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from demo_dashboard.dashboards.mistral import api + + +class ExecuteForm(forms.SelfHandlingForm): + workbook_name = forms.CharField(label=_("Workbook"), + required=True, + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + task = forms.CharField(label=_("Task"), + required=True, + help_text=_("Name of the task to stop")) + context = forms.CharField(label=_("Context"), + required=False, + initial="{}", + widget=forms.widgets.Textarea()) + + def __init__(self, request, *args, **kwargs): + super(ExecuteForm, self).__init__(request, *args, **kwargs) + + def handle(self, request, data): + try: + ex = api.mistralclient(request).executions.create(**data) + + msg = _('Execution has been created with id "%s".') % ex.id + messages.success(request, msg) + return True + except Exception: + msg = _('Failed to execute workbook "%s".') % data['workbook_name'] + redirect = reverse('horizon:mistral:workbooks:index') + exceptions.handle(request, msg, redirect=redirect) diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/panel.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/panel.py new file mode 100644 index 00000000..35bc126c --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/panel.py @@ -0,0 +1,13 @@ +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from demo_dashboard.dashboards.mistral import dashboard + + +class Workbooks(horizon.Panel): + name = _("Workbooks") + slug = 'workbooks' + + +dashboard.MistralDashboard.register(Workbooks) diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/tables.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/tables.py new file mode 100644 index 00000000..33f424ea --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/tables.py @@ -0,0 +1,28 @@ +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + + +class ExecuteWorkflow(tables.LinkAction): + name = "execute" + verbose_name = _("Execute") + url = "horizon:mistral:workbooks:execute" + classes = ("ajax-modal", "btn-edit") + + +def tags_to_string(workbook): + return ', '.join(workbook.tags) + + +class WorkbooksTable(tables.DataTable): + name = tables.Column("name", verbose_name=_("Name")) + description = tables.Column("description", verbose_name=_("Description")) + tags = tables.Column(tags_to_string, verbose_name=_("Tags")) + + def get_object_id(self, datum): + return datum.name + + class Meta: + name = "workbooks" + verbose_name = _("Workbooks") + row_actions = (ExecuteWorkflow,) diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/_execute.html b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/_execute.html new file mode 100644 index 00000000..94d3267c --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/_execute.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}execute_form{% endblock %} +{% block form_action %}{% url 'horizon:mistral:workbooks:execute' workbook_name %}{% endblock %} + +{% block modal-header %}{% trans "Execute" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "From here you can execute a workbook." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/execute.html b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/execute.html new file mode 100644 index 00000000..848d6c9c --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/execute.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Execute workbook" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Execute workbook") %} +{% endblock page_header %} + +{% block main %} + {% include 'mistral/workbooks/_execute.html' %} +{% endblock %} diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/index.html b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/index.html new file mode 100644 index 00000000..a7094d51 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/templates/workbooks/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Workbooks" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Workbooks") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/urls.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/urls.py new file mode 100644 index 00000000..098ff193 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import patterns # noqa +from django.conf.urls import url # noqa + +from demo_dashboard.dashboards.mistral.workbooks.views \ + import IndexView, ExecuteView + +WORKBOOKS = r'^(?P[^/]+)/%s$' + +urlpatterns = patterns( + '', + url(r'^$', IndexView.as_view(), name='index'), + url(WORKBOOKS % 'execute', ExecuteView.as_view(), name='execute'), +) diff --git a/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/views.py b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/views.py new file mode 100644 index 00000000..bd8a484a --- /dev/null +++ b/horizon_dashboard/demo_dashboard/dashboards/mistral/workbooks/views.py @@ -0,0 +1,31 @@ +from django.core.urlresolvers import reverse_lazy + +from horizon import tables, forms + +from demo_dashboard.dashboards.mistral import api +from demo_dashboard.dashboards.mistral.workbooks.tables import WorkbooksTable +from demo_dashboard.dashboards.mistral.workbooks.forms import ExecuteForm + + +class IndexView(tables.DataTableView): + table_class = WorkbooksTable + template_name = 'mistral/workbooks/index.html' + + def get_data(self): + return api.mistralclient(self.request).workbooks.list() + + +class ExecuteView(forms.ModalFormView): + form_class = ExecuteForm + template_name = 'mistral/workbooks/execute.html' + success_url = reverse_lazy("horizon:mistral:executions:index") + + def get_context_data(self, **kwargs): + context = super(ExecuteView, self).get_context_data(**kwargs) + context["workbook_name"] = self.kwargs['workbook_name'] + return context + + def get_initial(self, **kwargs): + return { + 'workbook_name': self.kwargs['workbook_name'] + } diff --git a/horizon_dashboard/demo_dashboard/local/__init__.py b/horizon_dashboard/demo_dashboard/local/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/horizon_dashboard/demo_dashboard/local/local_settings.py.example b/horizon_dashboard/demo_dashboard/local/local_settings.py.example new file mode 100644 index 00000000..d1869108 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/local/local_settings.py.example @@ -0,0 +1,488 @@ +import os + +from django.utils.translation import ugettext_lazy as _ + +from openstack_dashboard import exceptions + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +# Required for Django 1.5. +# If horizon is running in production (DEBUG is False), set this +# with the list of host/domain names that the application can serve. +# For more information see: +# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +#ALLOWED_HOSTS = ['horizon.example.com', ] + +# Set SSL proxy settings: +# For Django 1.4+ pass this header from the proxy after terminating the SSL, +# and don't forget to strip it from the client's request. +# For more information see: +# https://docs.djangoproject.com/en/1.4/ref/settings/#secure-proxy-ssl-header +# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') + +# If Horizon is being served through SSL, then uncomment the following two +# settings to better secure the cookies from security exploits +#CSRF_COOKIE_SECURE = True +#SESSION_COOKIE_SECURE = True + +# Overrides for OpenStack API versions. Use this setting to force the +# OpenStack dashboard to use a specific API version for a given service API. +# NOTE: The version should be formatted as it appears in the URL for the +# service API. For example, The identity service APIs have inconsistent +# use of the decimal point, so valid options would be "2.0" or "3". +# OPENSTACK_API_VERSIONS = { +# "identity": 3, +# "volume": 2 +# } + +# Set this to True if running on multi-domain model. When this is enabled, it +# will require user to enter the Domain name in addition to username for login. +# OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = False + +# Overrides the default domain used when running on single-domain model +# with Keystone V3. All entities will be created in the default domain. +# OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'Default' + +# Set Console type: +# valid options would be "AUTO", "VNC", "SPICE" or "RDP" +# CONSOLE_TYPE = "AUTO" + +# Default OpenStack Dashboard configuration. +HORIZON_CONFIG = { + 'dashboards': ('project', 'admin', 'settings',), + 'default_dashboard': 'project', + 'user_home': 'openstack_dashboard.views.get_user_home', + 'ajax_queue_limit': 10, + 'auto_fade_alerts': { + 'delay': 3000, + 'fade_duration': 1500, + 'types': ['alert-success', 'alert-info'] + }, + 'help_url': "http://docs.openstack.org", + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, +} + +# Specify a regular expression to validate user passwords. +# HORIZON_CONFIG["password_validator"] = { +# "regex": '.*', +# "help_text": _("Your password does not meet the requirements.") +# } + +# Disable simplified floating IP address management for deployments with +# multiple floating IP pools or complex network requirements. +# HORIZON_CONFIG["simple_ip_management"] = False + +# Turn off browser autocompletion for the login form if so desired. +# HORIZON_CONFIG["password_autocomplete"] = "off" + +LOCAL_PATH = os.path.dirname(os.path.abspath(__file__)) + +# Set custom secret key: +# You can either set it to a specific value or you can let horizion generate a +# default secret key that is unique on this machine, e.i. regardless of the +# amount of Python WSGI workers (if used behind Apache+mod_wsgi): However, there +# may be situations where you would want to set this explicitly, e.g. when +# multiple dashboard instances are distributed on different machines (usually +# behind a load-balancer). Either you have to make sure that a session gets all +# requests routed to the same dashboard instance or you set the same SECRET_KEY +# for all of them. +from horizon.utils import secret_key +SECRET_KEY = secret_key.generate_or_read_from_file( + os.path.join(LOCAL_PATH, '.secret_key_store')) + +# We recommend you use memcached for development; otherwise after every reload +# of the django development server, you will have to login again. To use +# memcached set CACHES to something like +# CACHES = { +# 'default': { +# 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', +# 'LOCATION': '127.0.0.1:11211', +# } +#} + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' + } +} + +# Send email to the console by default +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +# Or send them to /dev/null +#EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + +# Configure these for your outgoing email host +# EMAIL_HOST = 'smtp.my-company.com' +# EMAIL_PORT = 25 +# EMAIL_HOST_USER = 'djangomail' +# EMAIL_HOST_PASSWORD = 'top-secret!' + +# For multiple regions uncomment this configuration, and add (endpoint, title). +# AVAILABLE_REGIONS = [ +# ('http://cluster1.example.com:5000/v2.0', 'cluster1'), +# ('http://cluster2.example.com:5000/v2.0', 'cluster2'), +# ] + +OPENSTACK_HOST = "172.16.80.200" +OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_HOST +OPENSTACK_KEYSTONE_DEFAULT_ROLE = "_member_" + +# Disable SSL certificate checks (useful for self-signed certificates): +# OPENSTACK_SSL_NO_VERIFY = True + +# The CA certificate to use to verify SSL connections +# OPENSTACK_SSL_CACERT = '/path/to/cacert.pem' + +# The OPENSTACK_KEYSTONE_BACKEND settings can be used to identify the +# capabilities of the auth backend for Keystone. +# If Keystone has been configured to use LDAP as the auth backend then set +# can_edit_user to False and name to 'ldap'. +# +# TODO(tres): Remove these once Keystone has an API to identify auth backend. +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 +} + +#Setting this to True, will add a new "Retrieve Password" action on instance, +#allowing Admin session password retrieval/decryption. +#OPENSTACK_ENABLE_PASSWORD_RETRIEVE = False + +# The Xen Hypervisor has the ability to set the mount point for volumes +# attached to instances (other Hypervisors currently do not). Setting +# can_set_mount_point to True will add the option to set the mount point +# from the UI. +OPENSTACK_HYPERVISOR_FEATURES = { + 'can_set_mount_point': False, + 'can_set_password': False, +} + +# The OPENSTACK_NEUTRON_NETWORK settings can be used to enable optional +# services provided by neutron. Options currently available are load +# balancer service, security groups, quotas, VPN service. +OPENSTACK_NEUTRON_NETWORK = { + 'enable_lb': False, + 'enable_firewall': False, + 'enable_quotas': True, + 'enable_vpn': False, + # The profile_support option is used to detect if an external router can be + # configured via the dashboard. When using specific plugins the + # profile_support can be turned on if needed. + 'profile_support': None, + #'profile_support': 'cisco', +} + +# The OPENSTACK_IMAGE_BACKEND settings can be used to customize features +# in the OpenStack Dashboard related to the Image service, such as the list +# of supported image formats. +# OPENSTACK_IMAGE_BACKEND = { +# 'image_formats': [ +# ('', ''), +# ('aki', _('AKI - Amazon Kernel Image')), +# ('ami', _('AMI - Amazon Machine Image')), +# ('ari', _('ARI - Amazon Ramdisk Image')), +# ('iso', _('ISO - Optical Disk Image')), +# ('qcow2', _('QCOW2 - QEMU Emulator')), +# ('raw', _('Raw')), +# ('vdi', _('VDI')), +# ('vhd', _('VHD')), +# ('vmdk', _('VMDK')) +# ] +# } + +# The IMAGE_CUSTOM_PROPERTY_TITLES settings is used to customize the titles for +# image custom property attributes that appear on image detail pages. +IMAGE_CUSTOM_PROPERTY_TITLES = { + "architecture": _("Architecture"), + "kernel_id": _("Kernel ID"), + "ramdisk_id": _("Ramdisk ID"), + "image_state": _("Euca2ools state"), + "project_id": _("Project ID"), + "image_type": _("Image Type") +} + +# OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints +# in the Keystone service catalog. Use this setting when Horizon is running +# external to the OpenStack environment. The default is 'publicURL'. +#OPENSTACK_ENDPOINT_TYPE = "publicURL" + +# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the +# case that OPENSTACK_ENDPOINT_TYPE is not present in the endpoints +# in the Keystone service catalog. Use this setting when Horizon is running +# external to the OpenStack environment. The default is None. This +# value should differ from OPENSTACK_ENDPOINT_TYPE if used. +#SECONDARY_ENDPOINT_TYPE = "publicURL" + +# The number of objects (Swift containers/objects or images) to display +# on a single page before providing a paging element (a "more" link) +# to paginate results. +API_RESULT_LIMIT = 1000 +API_RESULT_PAGE_SIZE = 20 + +# The timezone of the server. This should correspond with the timezone +# of your entire OpenStack installation, and hopefully be in UTC. +TIME_ZONE = "UTC" + +# When launching an instance, the menu of available flavors is +# sorted by RAM usage, ascending. If you would like a different sort order, +# you can provide another flavor attribute as sorting key. Alternatively, you +# can provide a custom callback method to use for sorting. You can also provide +# a flag for reverse sort. For more info, see +# http://docs.python.org/2/library/functions.html#sorted +# CREATE_INSTANCE_FLAVOR_SORT = { +# 'key': 'name', +# # or +# 'key': my_awesome_callback_method, +# 'reverse': False, +# } + +# The Horizon Policy Enforcement engine uses these values to load per service +# policy rule files. The content of these files should match the files the +# OpenStack services are using to determine role based access control in the +# target installation. + +# Path to directory containing policy.json files +#POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf") +# Map of local copy of service policy files +#POLICY_FILES = { +# 'identity': 'keystone_policy.json', +# 'compute': 'nova_policy.json' +#} + +# Trove user and database extension support. By default support for +# creating users and databases on database instances is turned on. +# To disable these extensions set the permission here to something +# unusable such as ["!"]. +# TROVE_ADD_USER_PERMS = [] +# TROVE_ADD_DATABASE_PERMS = [] + +LOGGING = { + 'version': 1, + # When set to True this will disable all logging except + # for loggers specified in this configuration dictionary. Note that + # if nothing is specified here and disable_existing_loggers is True, + # django.db.backends will still log unless it is disabled explicitly. + 'disable_existing_loggers': False, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'django.utils.log.NullHandler', + }, + 'console': { + # Set the level to "DEBUG" for verbose output logging. + 'level': 'INFO', + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + # Logging from django.db.backends is VERY verbose, send to null + # by default. + 'django.db.backends': { + 'handlers': ['null'], + 'propagate': False, + }, + 'requests': { + 'handlers': ['null'], + 'propagate': False, + }, + 'horizon': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'openstack_dashboard': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'novaclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'cinderclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'keystoneclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'glanceclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'neutronclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'heatclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'ceilometerclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'troveclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'swiftclient': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'openstack_auth': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'nose.plugins.manager': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'django': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'iso8601': { + 'handlers': ['null'], + 'propagate': False, + }, + } +} + +# 'direction' should not be specified for all_tcp/udp/icmp. +# It is specified in the form. +SECURITY_GROUP_RULES = { + 'all_tcp': { + 'name': 'ALL TCP', + 'ip_protocol': 'tcp', + 'from_port': '1', + 'to_port': '65535', + }, + 'all_udp': { + 'name': 'ALL UDP', + 'ip_protocol': 'udp', + 'from_port': '1', + 'to_port': '65535', + }, + 'all_icmp': { + 'name': 'ALL ICMP', + 'ip_protocol': 'icmp', + 'from_port': '-1', + 'to_port': '-1', + }, + 'ssh': { + 'name': 'SSH', + 'ip_protocol': 'tcp', + 'from_port': '22', + 'to_port': '22', + }, + 'smtp': { + 'name': 'SMTP', + 'ip_protocol': 'tcp', + 'from_port': '25', + 'to_port': '25', + }, + 'dns': { + 'name': 'DNS', + 'ip_protocol': 'tcp', + 'from_port': '53', + 'to_port': '53', + }, + 'http': { + 'name': 'HTTP', + 'ip_protocol': 'tcp', + 'from_port': '80', + 'to_port': '80', + }, + 'pop3': { + 'name': 'POP3', + 'ip_protocol': 'tcp', + 'from_port': '110', + 'to_port': '110', + }, + 'imap': { + 'name': 'IMAP', + 'ip_protocol': 'tcp', + 'from_port': '143', + 'to_port': '143', + }, + 'ldap': { + 'name': 'LDAP', + 'ip_protocol': 'tcp', + 'from_port': '389', + 'to_port': '389', + }, + 'https': { + 'name': 'HTTPS', + 'ip_protocol': 'tcp', + 'from_port': '443', + 'to_port': '443', + }, + 'smtps': { + 'name': 'SMTPS', + 'ip_protocol': 'tcp', + 'from_port': '465', + 'to_port': '465', + }, + 'imaps': { + 'name': 'IMAPS', + 'ip_protocol': 'tcp', + 'from_port': '993', + 'to_port': '993', + }, + 'pop3s': { + 'name': 'POP3S', + 'ip_protocol': 'tcp', + 'from_port': '995', + 'to_port': '995', + }, + 'ms_sql': { + 'name': 'MS SQL', + 'ip_protocol': 'tcp', + 'from_port': '1433', + 'to_port': '1433', + }, + 'mysql': { + 'name': 'MYSQL', + 'ip_protocol': 'tcp', + 'from_port': '3306', + 'to_port': '3306', + }, + 'rdp': { + 'name': 'RDP', + 'ip_protocol': 'tcp', + 'from_port': '3389', + 'to_port': '3389', + }, +} + +FLAVOR_EXTRA_KEYS = { + 'flavor_keys': [ + ('quota:read_bytes_sec', _('Quota: Read bytes')), + ('quota:write_bytes_sec', _('Quota: Write bytes')), + ('quota:cpu_quota', _('Quota: CPU')), + ('quota:cpu_period', _('Quota: CPU period')), + ('quota:inbound_average', _('Quota: Inbound average')), + ('quota:outbound_average', _('Quota: Outbound average')), + ] +} diff --git a/horizon_dashboard/demo_dashboard/models.py b/horizon_dashboard/demo_dashboard/models.py new file mode 100644 index 00000000..1b3d5f9e --- /dev/null +++ b/horizon_dashboard/demo_dashboard/models.py @@ -0,0 +1,3 @@ +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/horizon_dashboard/demo_dashboard/settings.py b/horizon_dashboard/demo_dashboard/settings.py new file mode 100644 index 00000000..de0a6232 --- /dev/null +++ b/horizon_dashboard/demo_dashboard/settings.py @@ -0,0 +1,263 @@ +# 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. + +import logging +import os +import sys +import warnings + +from django.utils.translation import ugettext_lazy as _ + +from openstack_dashboard import exceptions + +warnings.formatwarning = lambda message, category, *args, **kwargs: \ + '%s: %s' % (category.__name__, message) + +ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) +BIN_DIR = os.path.abspath(os.path.join(ROOT_PATH, '..', 'bin')) + +if ROOT_PATH not in sys.path: + sys.path.append(ROOT_PATH) + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +SITE_BRANDING = 'OpenStack Dashboard' + +LOGIN_URL = '/auth/login/' +LOGOUT_URL = '/auth/logout/' +# LOGIN_REDIRECT_URL can be used as an alternative for +# HORIZON_CONFIG.user_home, if user_home is not set. +# Do not set it to '/home/', as this will cause circular redirect loop +LOGIN_REDIRECT_URL = '/' + +MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media')) +MEDIA_URL = '/media/' +STATIC_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'static')) +STATIC_URL = '/static/' + +ROOT_URLCONF = 'openstack_dashboard.urls' + +HORIZON_CONFIG = { + 'dashboards': ('project', 'admin', 'mistral', + 'settings', 'router',), + 'default_dashboard': 'project', + 'user_home': 'openstack_dashboard.views.get_user_home', + 'ajax_queue_limit': 10, + 'auto_fade_alerts': { + 'delay': 3000, + 'fade_duration': 1500, + 'types': ['alert-success', 'alert-info'] + }, + '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 + +# The OPENSTACK_IMAGE_BACKEND settings can be used to customize features +# in the OpenStack Dashboard related to the Image service, such as the list +# of supported image formats. +OPENSTACK_IMAGE_BACKEND = { + 'image_formats': [ + ('', ''), + ('aki', _('AKI - Amazon Kernel Image')), + ('ami', _('AMI - Amazon Machine Image')), + ('ari', _('ARI - Amazon Ramdisk Image')), + ('iso', _('ISO - Optical Disk Image')), + ('qcow2', _('QCOW2 - QEMU Emulator')), + ('raw', _('Raw')), + ('vdi', _('VDI')), + ('vhd', _('VHD')), + ('vmdk', _('VMDK')) + ] +} + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'horizon.middleware.HorizonMiddleware', + 'django.middleware.doc.XViewMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.request', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.contrib.messages.context_processors.messages', + 'horizon.context_processors.horizon', + 'openstack_dashboard.context_processors.openstack', +) + +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'horizon.loaders.TemplateLoader' +) + +TEMPLATE_DIRS = ( + os.path.join(ROOT_PATH, 'templates'), +) + +STATICFILES_FINDERS = ( + 'compressor.finders.CompressorFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) + +COMPRESS_PRECOMPILERS = ( + ('text/less', ('lesscpy {infile}')), +) + +COMPRESS_CSS_FILTERS = ( + 'compressor.filters.css_default.CssAbsoluteFilter', +) + +COMPRESS_ENABLED = True +COMPRESS_OUTPUT_DIR = 'dashboard' +COMPRESS_CSS_HASHING_METHOD = 'hash' +COMPRESS_PARSER = 'compressor.parser.HtmlParser' + +INSTALLED_APPS = [ + 'openstack_dashboard', + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'compressor', + 'horizon', + 'openstack_auth', + 'demo_dashboard.dashboards.mistral' +] + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',) +MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' + +SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' +SESSION_COOKIE_HTTPONLY = True +SESSION_EXPIRE_AT_BROWSER_CLOSE = True +SESSION_COOKIE_SECURE = False +SESSION_TIMEOUT = 1800 + +# When using cookie-based sessions, log error when the session cookie exceeds +# the following size (common browsers drop cookies above a certain size): +SESSION_COOKIE_MAX_SIZE = 4093 + +# when doing upgrades, it may be wise to stick to PickleSerializer +# TODO(mrunge): remove after Icehouse +SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' + +LANGUAGES = ( + ('de', 'German'), + ('en', 'English'), + ('en-au', 'Australian English'), + ('en-gb', 'British English'), + ('es', 'Spanish'), + ('fr', 'French'), + ('hi', 'Hindi'), + ('ja', 'Japanese'), + ('ko', 'Korean (Korea)'), + ('nl', 'Dutch (Netherlands)'), + ('pl', 'Polish'), + ('pt-br', 'Portuguese (Brazil)'), + ('sr', 'Serbian'), + ('zh-cn', 'Simplified Chinese'), + ('zh-tw', 'Chinese (Taiwan)'), +) +LANGUAGE_CODE = 'en' +LANGUAGE_COOKIE_NAME = 'horizon_language' +USE_I18N = True +USE_L10N = True +USE_TZ = True + +OPENSTACK_KEYSTONE_DEFAULT_ROLE = '_member_' + +DEFAULT_EXCEPTION_REPORTER_FILTER = 'horizon.exceptions.HorizonReporterFilter' + +POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf") +# Map of local copy of service policy files +POLICY_FILES = { + 'identity': 'keystone_policy.json', + 'compute': 'nova_policy.json', + 'volume': 'cinder_policy.json', + 'image': 'glance_policy.json', +} + +SECRET_KEY = None +LOCAL_PATH = None + +try: + from local.local_settings import * # noqa +except ImportError: + logging.warning("No local_settings file found.") + +# Load the pluggable dashboard settings +import openstack_dashboard.enabled +import openstack_dashboard.local.enabled +from openstack_dashboard.utils import settings + +INSTALLED_APPS = list(INSTALLED_APPS) # Make sure it's mutable +settings.update_dashboards([ + openstack_dashboard.enabled, + openstack_dashboard.local.enabled, +], HORIZON_CONFIG, INSTALLED_APPS) + +# Ensure that we always have a SECRET_KEY set, even when no local_settings.py +# file is present. See local_settings.py.example for full documentation on the +# horizon.utils.secret_key module and its use. +if not SECRET_KEY: + if not LOCAL_PATH: + LOCAL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'local') + + from horizon.utils import secret_key + SECRET_KEY = secret_key.generate_or_read_from_file(os.path.join(LOCAL_PATH, + '.secret_key_store')) + +from openstack_dashboard import policy +POLICY_CHECK_FUNCTION = policy.check + +# Add HORIZON_CONFIG to the context information for offline compression +COMPRESS_OFFLINE_CONTEXT = { + 'STATIC_URL': STATIC_URL, + 'HORIZON_CONFIG': HORIZON_CONFIG +} + +if DEBUG: + logging.basicConfig(level=logging.DEBUG) + +# during django reloads and an active user is logged in, the monkey +# patch below will not otherwise be applied in time - resulting in developers +# appearing to be logged out. In typical production deployments this section +# below may be ommited, though it should not be harmful +from openstack_auth import utils as auth_utils +auth_utils.patch_middleware_get_user() diff --git a/horizon_dashboard/manage.py b/horizon_dashboard/manage.py new file mode 100755 index 00000000..7193f129 --- /dev/null +++ b/horizon_dashboard/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +# 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 sys + +from django.core.management import execute_from_command_line # noqa + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_dashboard.settings") + execute_from_command_line(sys.argv) diff --git a/horizon_dashboard/requirements.txt b/horizon_dashboard/requirements.txt new file mode 100644 index 00000000..0434414f --- /dev/null +++ b/horizon_dashboard/requirements.txt @@ -0,0 +1,3 @@ +-e git+https://github.com/openstack/horizon.git#egg=horizon +# -e ../python-mistralclient +-e git+https://github.com/stackforge/python-mistralclient.git#egg=mistralclient diff --git a/horizon_dashboard/run_tests.sh b/horizon_dashboard/run_tests.sh new file mode 100755 index 00000000..fed8cd5a --- /dev/null +++ b/horizon_dashboard/run_tests.sh @@ -0,0 +1,502 @@ +#!/bin/bash + +set -o errexit + +# ---------------UPDATE ME-------------------------------# +# Increment me any time the environment should be rebuilt. +# This includes dependency changes, directory renames, etc. +# Simple integer sequence: 1, 2, 3... +environment_version=42 +#--------------------------------------------------------# + +function usage { + echo "Usage: $0 [OPTION]..." + echo "Run Horizon's test suite(s)" + echo "" + echo " -V, --virtual-env Always use virtualenv. Install automatically" + echo " if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local" + echo " environment" + echo " -c, --coverage Generate reports using Coverage" + echo " -f, --force Force a clean re-build of the virtual" + echo " environment. Useful when dependencies have" + echo " been added." + echo " -m, --manage Run a Django management command." + echo " --makemessages Create/Update English translation files." + echo " --compilemessages Compile all translation files." + echo " -p, --pep8 Just run pep8" + echo " -P, --no-pep8 Don't run pep8 by default" + echo " -t, --tabs Check for tab characters in files." + echo " -y, --pylint Just run pylint" + echo " -q, --quiet Run non-interactively. (Relatively) quiet." + echo " Implies -V if -N is not set." + echo " --only-selenium Run only the Selenium unit tests" + echo " --with-selenium Run unit tests including Selenium tests" + echo " --integration Run the integration tests (requires a running " + echo " OpenStack environment)" + echo " --runserver Run the Django development server for" + echo " openstack_dashboard in the virtual" + echo " environment." + echo " --docs Just build the documentation" + echo " --backup-environment Make a backup of the environment on exit" + echo " --restore-environment Restore the environment before running" + echo " --destroy-environment Destroy the environment and exit" + echo " -h, --help Print this usage message" + echo "" + echo "Note: with no options specified, the script will try to run the tests in" + echo " a virtual environment, If no virtualenv is found, the script will ask" + echo " if you would like to create one. If you prefer to run tests NOT in a" + echo " virtual environment, simply pass the -N option." + exit +} + +# DEFAULTS FOR RUN_TESTS.SH +# +root=`pwd -P` +venv=$root/.venv +with_venv=tools/with_venv.sh +included_dirs="demo_dashboard" +tested_apps="demo_dashboard" +settings="demo_dashboard.test.settings" + +always_venv=0 +backup_env=0 +command_wrapper="" +destroy=0 +force=0 +just_pep8=0 +no_pep8=0 +just_pylint=0 +just_docs=0 +just_tabs=0 +never_venv=0 +quiet=0 +restore_env=0 +runserver=0 +only_selenium=0 +with_selenium=0 +integration=0 +testopts="" +testargs="" +with_coverage=0 +makemessages=0 +compilemessages=0 +manage=0 + +# Jenkins sets a "JOB_NAME" variable, if it's not set, we'll make it "default" +[ "$JOB_NAME" ] || JOB_NAME="default" + +function process_option { + # If running manage command, treat the rest of options as arguments. + if [ $manage -eq 1 ]; then + testargs="$testargs $1" + return 0 + fi + + case "$1" in + -h|--help) usage;; + -V|--virtual-env) always_venv=1; never_venv=0;; + -N|--no-virtual-env) always_venv=0; never_venv=1;; + -p|--pep8) just_pep8=1;; + -P|--no-pep8) no_pep8=1;; + -y|--pylint) just_pylint=1;; + -f|--force) force=1;; + -t|--tabs) just_tabs=1;; + -q|--quiet) quiet=1;; + -c|--coverage) with_coverage=1;; + -m|--manage) manage=1;; + --makemessages) makemessages=1;; + --compilemessages) compilemessages=1;; + --only-selenium) only_selenium=1;; + --with-selenium) with_selenium=1;; + --integration) integration=1;; + --docs) just_docs=1;; + --runserver) runserver=1;; + --backup-environment) backup_env=1;; + --restore-environment) restore_env=1;; + --destroy-environment) destroy=1;; + -*) testopts="$testopts $1";; + *) testargs="$testargs $1" + esac +} + +function run_management_command { + ${command_wrapper} python $root/manage.py $testopts $testargs +} + +function run_server { + echo "Starting Django development server..." + ${command_wrapper} python $root/manage.py runserver $testopts $testargs + echo "Server stopped." +} + +function run_pylint { + echo "Running pylint ..." + PYTHONPATH=$root ${command_wrapper} pylint --rcfile=.pylintrc -f parseable $included_dirs > pylint.txt || true + CODE=$? + grep Global -A2 pylint.txt + if [ $CODE -lt 32 ]; then + echo "Completed successfully." + exit 0 + else + echo "Completed with problems." + exit $CODE + fi +} + +function run_pep8 { + echo "Running flake8 ..." + set +o errexit + ${command_wrapper} python -c "import hacking" 2>/dev/null + no_hacking=$? + set -o errexit + if [ $never_venv -eq 1 -a $no_hacking -eq 1 ]; then + echo "**WARNING**:" >&2 + echo "OpenStack hacking is not installed on your host. Its detection will be missed." >&2 + echo "Please install or use virtual env if you need OpenStack hacking detection." >&2 + fi + DJANGO_SETTINGS_MODULE=openstack_dashboard.test.settings ${command_wrapper} flake8 +} + +function run_sphinx { + echo "Building sphinx..." + export DJANGO_SETTINGS_MODULE=openstack_dashboard.settings + ${command_wrapper} sphinx-build -b html doc/source doc/build/html + echo "Build complete." +} + +function tab_check { + TAB_VIOLATIONS=`find $included_dirs -type f -regex ".*\.\(css\|js\|py\|html\)" -print0 | xargs -0 awk '/\t/' | wc -l` + if [ $TAB_VIOLATIONS -gt 0 ]; then + echo "TABS! $TAB_VIOLATIONS of them! Oh no!" + HORIZON_FILES=`find $included_dirs -type f -regex ".*\.\(css\|js\|py|\html\)"` + for TABBED_FILE in $HORIZON_FILES + do + TAB_COUNT=`awk '/\t/' $TABBED_FILE | wc -l` + if [ $TAB_COUNT -gt 0 ]; then + echo "$TABBED_FILE: $TAB_COUNT" + fi + done + fi + return $TAB_VIOLATIONS; +} + +function destroy_venv { + echo "Cleaning environment..." + echo "Removing virtualenv..." + rm -rf $venv + echo "Virtualenv removed." + rm -f .environment_version + echo "Environment cleaned." +} + +function environment_check { + echo "Checking environment." + if [ -f .environment_version ]; then + ENV_VERS=`cat .environment_version` + if [ $ENV_VERS -eq $environment_version ]; then + if [ -e ${venv} ]; then + # If the environment exists and is up-to-date then set our variables + command_wrapper="${root}/${with_venv}" + echo "Environment is up to date." + return 0 + fi + fi + fi + + if [ $always_venv -eq 1 ]; then + install_venv + else + if [ ! -e ${venv} ]; then + echo -e "Environment not found. Install? (Y/n) \c" + else + echo -e "Your environment appears to be out of date. Update? (Y/n) \c" + fi + read update_env + if [ "x$update_env" = "xY" -o "x$update_env" = "x" -o "x$update_env" = "xy" ]; then + install_venv + else + # Set our command wrapper anyway. + command_wrapper="${root}/${with_venv}" + fi + fi +} + +function sanity_check { + # Anything that should be determined prior to running the tests, server, etc. + # Don't sanity-check anything environment-related in -N flag is set + if [ $never_venv -eq 0 ]; then + if [ ! -e ${venv} ]; then + echo "Virtualenv not found at $venv. Did install_venv.py succeed?" + exit 1 + fi + fi + # Remove .pyc files. This is sanity checking because they can linger + # after old files are deleted. + find . -name "*.pyc" -exec rm -rf {} \; +} + +function backup_environment { + if [ $backup_env -eq 1 ]; then + echo "Backing up environment \"$JOB_NAME\"..." + if [ ! -e ${venv} ]; then + echo "Environment not installed. Cannot back up." + return 0 + fi + if [ -d /tmp/.horizon_environment/$JOB_NAME ]; then + mv /tmp/.horizon_environment/$JOB_NAME /tmp/.horizon_environment/$JOB_NAME.old + rm -rf /tmp/.horizon_environment/$JOB_NAME + fi + mkdir -p /tmp/.horizon_environment/$JOB_NAME + cp -r $venv /tmp/.horizon_environment/$JOB_NAME/ + cp .environment_version /tmp/.horizon_environment/$JOB_NAME/ + # Remove the backup now that we've completed successfully + rm -rf /tmp/.horizon_environment/$JOB_NAME.old + echo "Backup completed" + fi +} + +function restore_environment { + if [ $restore_env -eq 1 ]; then + echo "Restoring environment from backup..." + if [ ! -d /tmp/.horizon_environment/$JOB_NAME ]; then + echo "No backup to restore from." + return 0 + fi + + cp -r /tmp/.horizon_environment/$JOB_NAME/.venv ./ || true + cp -r /tmp/.horizon_environment/$JOB_NAME/.environment_version ./ || true + + echo "Environment restored successfully." + fi +} + +function install_venv { + # Install with install_venv.py + export PIP_DOWNLOAD_CACHE=${PIP_DOWNLOAD_CACHE-/tmp/.pip_download_cache} + export PIP_USE_MIRRORS=true + if [ $quiet -eq 1 ]; then + export PIP_NO_INPUT=true + fi + echo "Fetching new src packages..." + rm -rf $venv/src + python tools/install_venv.py + command_wrapper="$root/${with_venv}" + # Make sure it worked and record the environment version + sanity_check + chmod -R 754 $venv + echo $environment_version > .environment_version +} + +function run_tests { + sanity_check + + if [ $with_selenium -eq 1 ]; then + export WITH_SELENIUM=1 + elif [ $only_selenium -eq 1 ]; then + export WITH_SELENIUM=1 + export SKIP_UNITTESTS=1 + fi + + if [ $with_selenium -eq 0 -a $integration -eq 0 ]; then + testopts="$testopts --exclude-dir=openstack_dashboard/test/integration_tests" + fi + + if [ -z "$testargs" ]; then + run_tests_all + else + run_tests_subset + fi +} + +function run_tests_subset { + project=`echo $testargs | awk -F. '{print $1}'` + ${command_wrapper} python $root/manage.py test --settings=$project.test.settings $testopts $testargs +} + +function run_tests_all { + echo "Running Horizon application tests" + export NOSE_XUNIT_FILE=horizon/nosetests.xml + if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then + export NOSE_HTML_OUT_FILE='horizon_nose_results.html' + fi + if [ $with_coverage -eq 1 ]; then + ${command_wrapper} coverage erase + coverage_run="coverage run -p" + fi + ${command_wrapper} ${coverage_run} $root/manage.py test horizon --settings=horizon.test.settings $testopts + # get results of the Horizon tests + HORIZON_RESULT=$? + + echo "Running openstack_dashboard tests" + export NOSE_XUNIT_FILE=openstack_dashboard/nosetests.xml + if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then + export NOSE_HTML_OUT_FILE='dashboard_nose_results.html' + fi + ${command_wrapper} ${coverage_run} $root/manage.py test openstack_dashboard --settings=openstack_dashboard.test.settings $testopts + # get results of the openstack_dashboard tests + DASHBOARD_RESULT=$? + + if [ $with_coverage -eq 1 ]; then + echo "Generating coverage reports" + ${command_wrapper} coverage combine + ${command_wrapper} coverage xml -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' + ${command_wrapper} coverage html -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' -d reports + fi + # Remove the leftover coverage files from the -p flag earlier. + rm -f .coverage.* + + PEP8_RESULT=0 + if [ $no_pep8 -eq 0 ] && [ $only_selenium -eq 0 ]; then + run_pep8 + PEP8_RESULT=$? + fi + + TEST_RESULT=$(($HORIZON_RESULT || $DASHBOARD_RESULT || $PEP8_RESULT)) + if [ $TEST_RESULT -eq 0 ]; then + echo "Tests completed successfully." + else + echo "Tests failed." + fi + exit $TEST_RESULT +} + +function run_integration_tests { + export INTEGRATION_TESTS=1 + + echo "Running Horizon integration tests..." + ${command_wrapper} nosetests openstack_dashboard/test/integration_tests/tests + exit 0 +} + +function run_makemessages { + OPTS="-l en --no-obsolete" + DASHBOARD_OPTS="--extension=html,txt,csv --ignore=openstack" + echo -n "horizon: " + cd horizon + ${command_wrapper} $root/manage.py makemessages $OPTS + HORIZON_PY_RESULT=$? + echo -n "horizon javascript: " + ${command_wrapper} $root/manage.py makemessages -d djangojs $OPTS + HORIZON_JS_RESULT=$? + echo -n "openstack_dashboard: " + cd ../openstack_dashboard + ${command_wrapper} $root/manage.py makemessages $DASHBOARD_OPTS $OPTS + DASHBOARD_RESULT=$? + cd .. + exit $(($HORIZON_PY_RESULT || $HORIZON_JS_RESULT || $DASHBOARD_RESULT)) +} + +function run_compilemessages { + cd horizon + ${command_wrapper} $root/manage.py compilemessages + HORIZON_PY_RESULT=$? + cd ../openstack_dashboard + ${command_wrapper} $root/manage.py compilemessages + DASHBOARD_RESULT=$? + cd .. + # English is the source language, so compiled catalogs are unnecessary. + rm -vf horizon/locale/en/LC_MESSAGES/django*.mo + rm -vf openstack_dashboard/locale/en/LC_MESSAGES/django.mo + exit $(($HORIZON_PY_RESULT || $DASHBOARD_RESULT)) +} + + +# ---------PREPARE THE ENVIRONMENT------------ # + +# PROCESS ARGUMENTS, OVERRIDE DEFAULTS +for arg in "$@"; do + process_option $arg +done + +if [ $quiet -eq 1 ] && [ $never_venv -eq 0 ] && [ $always_venv -eq 0 ] +then + always_venv=1 +fi + +# If destroy is set, just blow it away and exit. +if [ $destroy -eq 1 ]; then + destroy_venv + exit 0 +fi + +# Ignore all of this if the -N flag was set +if [ $never_venv -eq 0 ]; then + + # Restore previous environment if desired + if [ $restore_env -eq 1 ]; then + restore_environment + fi + + # Remove the virtual environment if --force used + if [ $force -eq 1 ]; then + destroy_venv + fi + + # Then check if it's up-to-date + environment_check + + # Create a backup of the up-to-date environment if desired + if [ $backup_env -eq 1 ]; then + backup_environment + fi +fi + +# ---------EXERCISE THE CODE------------ # + +# Run management commands +if [ $manage -eq 1 ]; then + run_management_command + exit $? +fi + +# Build the docs +if [ $just_docs -eq 1 ]; then + run_sphinx + exit $? +fi + +# Update translation files +if [ $makemessages -eq 1 ]; then + run_makemessages + exit $? +fi + +# Compile translation files +if [ $compilemessages -eq 1 ]; then + run_compilemessages + exit $? +fi + +# PEP8 +if [ $just_pep8 -eq 1 ]; then + run_pep8 + exit $? +fi + +# Pylint +if [ $just_pylint -eq 1 ]; then + run_pylint + exit $? +fi + +# Tab checker +if [ $just_tabs -eq 1 ]; then + tab_check + exit $? +fi + +# Integration tests +if [ $integration -eq 1 ]; then + run_integration_tests + exit $? +fi + +# Django development server +if [ $runserver -eq 1 ]; then + run_server + exit $? +fi + +# Full test suite +run_tests || exit diff --git a/horizon_dashboard/test-requirements.txt b/horizon_dashboard/test-requirements.txt new file mode 100644 index 00000000..897b992d --- /dev/null +++ b/horizon_dashboard/test-requirements.txt @@ -0,0 +1,8 @@ +coverage +django-nose +mox +netaddr +nose +pep8 +pylint +sphinx \ No newline at end of file diff --git a/horizon_dashboard/tools/install_venv.py b/horizon_dashboard/tools/install_venv.py new file mode 100644 index 00000000..0342f97d --- /dev/null +++ b/horizon_dashboard/tools/install_venv.py @@ -0,0 +1,71 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2010 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 sys +import install_venv_common as install_venv # noqa +import os + + + +def print_help(venv, root): + help = """ + Openstack development environment setup is complete. + + Openstack development uses virtualenv to track and manage Python + dependencies while in development and testing. + + To activate the Openstack virtualenv for the extent of your current shell + session you can run: + + $ source %s/bin/activate + + Or, if you prefer, you can run commands in the virtualenv on a case by case + basis by running: + + $ %s/tools/with_venv.sh + + Also, make test will automatically use the virtualenv. + """ + print(help % (venv, root)) + + +def main(argv): + root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + + if os.environ.get('tools_path'): + root = os.environ['tools_path'] + venv = os.path.join(root, '.venv') + if os.environ.get('venv'): + venv = os.environ['venv'] + + pip_requires = os.path.join(root, 'requirements.txt') + test_requires = os.path.join(root, 'test-requirements.txt') + py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) + project = 'Openstack' + install = install_venv.InstallVenv(root, venv, pip_requires, test_requires, + py_version, project) + options = install.parse_args(argv) + install.check_python_version() + install.check_dependencies() + install.create_virtualenv(no_site_packages=options.no_site_packages) + install.install_dependencies() + print_help(venv, root) + +if __name__ == '__main__': + main(sys.argv) diff --git a/horizon_dashboard/tools/install_venv_common.py b/horizon_dashboard/tools/install_venv_common.py new file mode 100644 index 00000000..46822e32 --- /dev/null +++ b/horizon_dashboard/tools/install_venv_common.py @@ -0,0 +1,172 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +"""Provides methods needed by installation script for OpenStack development +virtual environments. + +Since this script is used to bootstrap a virtualenv from the system's Python +environment, it should be kept strictly compatible with Python 2.6. + +Synced in from openstack-common +""" + +from __future__ import print_function + +import optparse +import os +import subprocess +import sys + + +class InstallVenv(object): + + def __init__(self, root, venv, requirements, + test_requirements, py_version, + project): + self.root = root + self.venv = venv + self.requirements = requirements + self.test_requirements = test_requirements + self.py_version = py_version + self.project = project + + def die(self, message, *args): + print(message % args, file=sys.stderr) + sys.exit(1) + + def check_python_version(self): + if sys.version_info < (2, 6): + self.die("Need Python Version >= 2.6") + + def run_command_with_code(self, cmd, redirect_output=True, + check_exit_code=True): + """Runs a command in an out-of-process shell. + + Returns the output of that command. Working directory is self.root. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return (output, proc.returncode) + + def run_command(self, cmd, redirect_output=True, check_exit_code=True): + return self.run_command_with_code(cmd, redirect_output, + check_exit_code)[0] + + def get_distro(self): + if (os.path.exists('/etc/fedora-release') or + os.path.exists('/etc/redhat-release')): + return Fedora( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + else: + return Distro( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + + def check_dependencies(self): + self.get_distro().install_virtualenv() + + def create_virtualenv(self, no_site_packages=True): + """Creates the virtual environment and installs PIP. + + Creates the virtual environment and installs PIP only into the + virtual environment. + """ + if not os.path.isdir(self.venv): + print('Creating venv...', end=' ') + if no_site_packages: + self.run_command(['virtualenv', '-q', '--no-site-packages', + self.venv]) + else: + self.run_command(['virtualenv', '-q', self.venv]) + print('done.') + else: + print("venv already exists...") + pass + + def pip_install(self, *args): + self.run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + def install_dependencies(self): + print('Installing dependencies with pip (this can take a while)...') + + # First things first, make sure our venv has the latest pip and + # setuptools and pbr + self.pip_install('pip>=1.4') + self.pip_install('setuptools') + self.pip_install('pbr') + + self.pip_install('-r', self.requirements, '-r', self.test_requirements) + + def parse_args(self, argv): + """Parses command-line arguments.""" + parser = optparse.OptionParser() + parser.add_option('-n', '--no-site-packages', + action='store_true', + help="Do not inherit packages from global Python " + "install") + return parser.parse_args(argv[1:])[0] + + +class Distro(InstallVenv): + + def check_cmd(self, cmd): + return bool(self.run_command(['which', cmd], + check_exit_code=False).strip()) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if self.check_cmd('easy_install'): + print('Installing virtualenv via easy_install...', end=' ') + if self.run_command(['easy_install', 'virtualenv']): + print('Succeeded') + return + else: + print('Failed') + + self.die('ERROR: virtualenv not found.\n\n%s development' + ' requires virtualenv, please install it using your' + ' favorite package management tool' % self.project) + + +class Fedora(Distro): + """This covers all Fedora-based distributions. + + Includes: Fedora, RHEL, CentOS, Scientific Linux + """ + + def check_pkg(self, pkg): + return self.run_command_with_code(['rpm', '-q', pkg], + check_exit_code=False)[1] == 0 + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if not self.check_pkg('python-virtualenv'): + self.die("Please install 'python-virtualenv'.") + + super(Fedora, self).install_virtualenv() diff --git a/horizon_dashboard/tools/requirements_style_check.sh b/horizon_dashboard/tools/requirements_style_check.sh new file mode 100755 index 00000000..ccbff3bd --- /dev/null +++ b/horizon_dashboard/tools/requirements_style_check.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# Enforce the requirement that dependencies are listed in the input +# files in alphabetical order. + +# FIXME(dhellmann): This doesn't deal with URL requirements very +# well. We should probably sort those on the egg-name, rather than the +# full line. + +function check_file() { + typeset f=$1 + + # We don't care about comment lines. + grep -v '^#' $f > ${f}.unsorted + sort -i -f ${f}.unsorted > ${f}.sorted + diff -c ${f}.unsorted ${f}.sorted + rc=$? + rm -f ${f}.sorted ${f}.unsorted + return $rc +} + +exit_code=0 +for filename in $@ +do + check_file $filename + if [ $? -ne 0 ] + then + echo "Please list requirements in $filename in alphabetical order" 1>&2 + exit_code=1 + fi +done +exit $exit_code diff --git a/horizon_dashboard/tools/with_venv.sh b/horizon_dashboard/tools/with_venv.sh new file mode 100755 index 00000000..7303990b --- /dev/null +++ b/horizon_dashboard/tools/with_venv.sh @@ -0,0 +1,7 @@ +#!/bin/bash +TOOLS_PATH=${TOOLS_PATH:-$(dirname $0)} +VENV_PATH=${VENV_PATH:-${TOOLS_PATH}} +VENV_DIR=${VENV_NAME:-/../.venv} +TOOLS=${TOOLS_PATH} +VENV=${VENV:-${VENV_PATH}/${VENV_DIR}} +source ${VENV}/bin/activate && "$@"